MAss diff changes imported from Ash's local diff-refactor branch
[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
35/;
36
37__PACKAGE__->mk_accessors(@diff_arrays, 'table_diff_hash');
38
39sub schema_diff {
da5a1bae 40 # use Data::Dumper;
41 ## we are getting instructions on how to turn the source into the target
42 ## source == original, target == new (hmm, if I need to comment this, should I rename the vars again ??)
43 ## _schema isa SQL::Translator::Schema
44 ## _db is the name of the producer/db it came out of/into
45 ## results are formatted to the source preferences
46
51ffe5ee 47 my ($source_schema, $source_db, $target_schema, $target_db, $options) = @_;
4d438549 48 $options ||= {};
da5a1bae 49
4d438549 50 my $obj = SQL::Translator::Diff->new( {
51 %$options,
52 source_schema => $source_schema,
53 source_db => $source_db,
54 target_schema => $target_schema,
55 target_db => $target_db
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
66 $values->{output_db} ||= $values->{source_db};
67 return $class->SUPER::new($values);
68}
69
70sub compute_differences {
71 my ($self) = @_;
51ffe5ee 72
4d438549 73 my $target_schema = $self->target_schema;
74 my $source_schema = $self->source_schema;
75
76 my @tar_tables = sort { $a->name cmp $b->name } $target_schema->get_tables;
da5a1bae 77 ## do original/source tables exist in target?
4d438549 78 for my $tar_table ( @tar_tables ) {
da5a1bae 79 my $tar_table_name = $tar_table->name;
4d438549 80 my $src_table = $source_schema->get_table( $tar_table_name, $self->case_insensitive );
da5a1bae 81
da5a1bae 82 unless ( $src_table ) {
da5a1bae 83 ## table is new
84 ## add table(s) later.
4d438549 85 push @{$self->tables_to_create}, $tar_table;
da5a1bae 86 next;
87 }
51ffe5ee 88
4d438549 89 $self->table_diff_hash->{$tar_table_name} = {
90 map {$_ => [] } @diff_hash_keys
91 };
92
93 $self->diff_table_options($src_table, $tar_table);
51ffe5ee 94
da5a1bae 95 ## Compare fields, their types, defaults, sizes etc etc
4d438549 96 $self->diff_table_fields($src_table, $tar_table);
51ffe5ee 97
4d438549 98 $self->diff_table_indexes($src_table, $tar_table);
99 $self->diff_table_constraints($src_table, $tar_table);
51ffe5ee 100
4d438549 101 } # end of target_schema->get_tables loop
51ffe5ee 102
4d438549 103 for my $src_table ( $source_schema->get_tables ) {
104 my $src_table_name = $src_table->name;
105 my $tar_table = $target_schema->get_table( $src_table_name, $self->case_insensitive );
51ffe5ee 106
4d438549 107 unless ( $tar_table ) {
108 $self->table_diff_hash->{$src_table_name} = {
109 map {$_ => [] } @diff_hash_keys
110 };
51ffe5ee 111
4d438549 112 push @{ $self->tables_to_drop}, $src_table;
113 next;
da5a1bae 114 }
4d438549 115 }
116
117 return $self;
118}
119
120sub produce_diff_sql {
121 my ($self) = @_;
122
123 my $target_schema = $self->target_schema;
124 my $source_schema = $self->source_schema;
125 my $tar_name = $target_schema->name;
126 my $src_name = $source_schema->name;
127
128 my $producer_class = "SQL::Translator::Producer::@{[$self->output_db]}";
129 eval "require $producer_class";
130 die $@ if $@;
131
132 # Map of name we store under => producer method name
133 my %func_map = (
134 constraints_to_create => 'alter_create_constraint',
135 constraints_to_drop => 'alter_drop_constraint',
136 indexes_to_create => 'alter_create_index',
137 indexes_to_drop => 'alter_drop_index',
138 fields_to_create => 'add_field',
139 fields_to_alter => 'alter_field',
140 fields_to_rename => 'rename_field',
141 fields_to_drop => 'drop_field',
142 table_options => 'alter_table'
143 );
144 my @diffs;
145
146 if (!$self->no_batch_alters &&
147 (my $batch_alter = $producer_class->can('batch_alter_table')) )
148 {
149 # Good - Producer supports batch altering of tables.
150 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
151 my $tar_table = $target_schema->get_table($table)
152 || $source_schema->get_table($table);
da5a1bae 153
4d438549 154 $DB::single = 1 if $table eq 'deleted';
155 push @diffs, $batch_alter->($tar_table,
156 { map {
157 $func_map{$_} => $self->table_diff_hash->{$table}{$_}
158 } keys %func_map
51ffe5ee 159 }
4d438549 160 );
da5a1bae 161 }
4d438549 162 } else {
51ffe5ee 163
4d438549 164 my %flattened_diffs;
165 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
166 my $table_diff = $self->table_diff_hash->{$table};
167 for (@diff_hash_keys) {
168 push( @{ $flattened_diffs{ $func_map{$_} } ||= [] }, @{ $table_diff->{$_} } );
51ffe5ee 169 }
4d438549 170 }
da5a1bae 171
4d438549 172 push @diffs, map( {
173 if (@{$flattened_diffs{$_}}) {
174 my $meth = $producer_class->can($_);
175
176 $meth ? map { my $sql = $meth->(ref $_ eq 'ARRAY' ? @$_ : $_); $sql ? ("$sql;") : () } @{ $flattened_diffs{$_} }
177 : $self->ignore_missing_methods
178 ? "-- $producer_class cant $_"
179 : die "$producer_class cant $_";
180 } else { () }
51ffe5ee 181
4d438549 182 } qw/alter_drop_constraint
183 alter_drop_index
184 drop_field
185 add_field
186 alter_field
187 rename_field
188 alter_create_index
189 alter_create_constraint
190 alter_table/),
da5a1bae 191 }
51ffe5ee 192
4d438549 193 if (my @tables = @{ $self->tables_to_create } ) {
194 my $translator = new SQL::Translator(
195 producer_type => $self->output_db,
196 add_drop_table => 0,
197 no_comments => 1,
198 # TODO: sort out options
199 quote_table_names => 0,
200 quote_field_names => 0,
201 );
202 my $schema = $translator->schema;
51ffe5ee 203
4d438549 204 $schema->add_table($_) for @tables;
51ffe5ee 205
4d438549 206 unshift @diffs,
207 # Remove begin/commit here, since we wrap everything in one.
208 grep { $_ !~ /^(?:COMMIT|BEGIN(?: TRANSACTION)?);/ } $producer_class->can('produce')->($translator);
7ac784ff 209 }
210
4d438549 211 if (my @tables_to_drop = @{ $self->{tables_to_drop} || []} ) {
212 my $meth = $producer_class->can('drop_table');
213
214 push @diffs, $meth ? map( { $meth->($_) } @tables_to_drop )
215 : $self->ignore_missing_methods
216 ? "-- $producer_class cant drop_table"
217 : die "$producer_class cant drop_table";
218 }
7ac784ff 219
4d438549 220 if (@diffs) {
221 unshift @diffs, "BEGIN TRANSACTION;\n";
222 push @diffs, "\nCOMMIT;\n";
223 } else {
224 @diffs = ("-- No differences found\n\n");
51ffe5ee 225 }
51ffe5ee 226
da5a1bae 227 if ( @diffs ) {
4d438549 228 if ( $self->target_db !~ /^(?:MySQL|SQLite)$/ ) {
229 unshift(@diffs, "-- Target database @{[$self->target_db]} is untested/unsupported!!!");
51ffe5ee 230 }
4d438549 231 return join( "\n", "-- Convert schema '$src_name' to '$tar_name':\n", @diffs);
51ffe5ee 232 }
da5a1bae 233 return undef;
4d438549 234
235}
236
237sub diff_table_indexes {
238 my ($self, $src_table, $tar_table) = @_;
239
240 my (%checked_indices);
241 INDEX_CREATE:
242 for my $i_tar ( $tar_table->get_indices ) {
243 for my $i_src ( $src_table->get_indices ) {
244 if ( $i_tar->equals($i_src, $self->case_insensitive, $self->ignore_index_names) ) {
245 $checked_indices{$i_src} = 1;
246 next INDEX_CREATE;
247 }
248 }
249 push @{$self->table_diff_hash->{$tar_table}{indexes_to_create}}, $i_tar;
250 }
251
252 INDEX_DROP:
253 for my $i_src ( $src_table->get_indices ) {
254 next if !$self->ignore_index_names && $checked_indices{$i_src};
255 for my $i_tar ( $tar_table->get_indices ) {
256 next INDEX_DROP if $i_src->equals($i_tar, $self->case_insensitive, $self->ignore_index_names);
257 }
258 push @{$self->table_diff_hash->{$tar_table}{indexes_to_drop}}, $i_src;
259 }
260}
261
262
263sub diff_table_constraints {
264 my ($self, $src_table, $tar_table) = @_;
265
266 my(%checked_constraints);
267 CONSTRAINT_CREATE:
268 for my $c_tar ( $tar_table->get_constraints ) {
269 for my $c_src ( $src_table->get_constraints ) {
270 if ( $c_tar->equals($c_src, $self->case_insensitive, $self->ignore_constraint_names) ) {
271 $checked_constraints{$c_src} = 1;
272 next CONSTRAINT_CREATE;
273 }
274 }
275 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_create} }, $c_tar;
276 }
277
278
279 CONSTRAINT_DROP:
280 for my $c_src ( $src_table->get_constraints ) {
281 next if !$self->ignore_constraint_names && $checked_constraints{$c_src};
282 for my $c_tar ( $tar_table->get_constraints ) {
283 next CONSTRAINT_DROP if $c_src->equals($c_tar, $self->case_insensitive, $self->ignore_constraint_names);
284 }
285
286 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_drop} }, $c_src;
51ffe5ee 287 }
51ffe5ee 288
4d438549 289}
290
291sub diff_table_fields {
292 my ($self, $src_table, $tar_table) = @_;
293
294 # List of ones ew've renamed from so we dont drop them
295 my %renamed_source_fields;
296
297 for my $tar_table_field ( $tar_table->get_fields ) {
298 my $f_tar_name = $tar_table_field->name;
299
300 if (my $old_name = $tar_table_field->extra->{renamed_from}) {
301 my $src_table_field = $src_table->get_field( $old_name, $self->case_insensitive );
302 die qq#Renamed cant find "@{[$src_table->name]}.$old_name" for renamed column\n# unless $src_table_field;
303 push @{$self->table_diff_hash->{$tar_table}{fields_to_rename} }, [ $src_table_field, $tar_table_field ];
304 $renamed_source_fields{$old_name} = 1;
305 next;
306 }
307
308 my $src_table_field = $src_table->get_field( $f_tar_name, $self->case_insensitive );
309
310 unless ( $src_table_field ) {
311 push @{$self->table_diff_hash->{$tar_table}{fields_to_create}}, $tar_table_field;
312 next;
313 }
314
315 ## field exists, something changed.
316 if ( !$tar_table_field->equals($src_table_field, $self->case_insensitive) ) {
317
318 # Some producers might need src field to diff against
319 push @{$self->table_diff_hash->{$tar_table}{fields_to_alter}}, [ $src_table_field, $tar_table_field ];
320 next;
321 }
322 }
323
324
325 # Now check to see if any fields from src_table need to be dropped
326 for my $src_table_field ( $src_table->get_fields ) {
327 my $f_src_name = $src_table_field->name;
328 next if $renamed_source_fields{$f_src_name};
329
330 my $tar_table_field = $tar_table->get_field( $f_src_name, $self->case_insensitive );
331
332 unless ( $tar_table_field ) {
333 push @{$self->table_diff_hash->{$tar_table}{fields_to_drop}}, $src_table_field;
334 next;
335 }
336 }
337}
338
339sub diff_table_options {
340 my ($self, $src_table, $tar_table) = @_;
341
342
343 # Go through our options
344 my $options_different = 0;
345 my %checkedOptions;
346
347 OPTION:
348 for my $tar_table_option_ref ( $tar_table->options ) {
349 my($key_tar, $value_tar) = %{$tar_table_option_ref};
350 for my $src_table_option_ref ( $src_table->options ) {
351 my($key_src, $value_src) = %{$src_table_option_ref};
352 if ( $key_tar eq $key_src ) {
353 if ( defined $value_tar != defined $value_src ) {
354 $options_different = 1;
355 last OPTION;
356 }
357 if ( defined $value_tar && $value_tar ne $value_src ) {
358 $options_different = 1;
359 last OPTION;
360 }
361 $checkedOptions{$key_tar} = 1;
362 next OPTION;
363 }
364 }
365 $options_different = 1;
366 last OPTION;
367 }
368
369 # Go through the other table's options
370 unless ( $options_different ) {
371 for my $src_table_option_ref ( $src_table->options ) {
372 my($key, $value) = %{$src_table_option_ref};
373 next if $checkedOptions{$key};
374 $options_different = 1;
375 last;
376 }
377 }
378
379 # If there's a difference, just re-set all the options
380 push @{ $self->table_diff_hash->{$tar_table}{table_options} }, $tar_table
381 if ( $options_different );
382}
383
51ffe5ee 3841;
4d438549 385
386__END__
387
388=head1 NAME
389
390SQL::Translator::Diff
391
392=head1 DESCRIPTION
393
394Takes two input SQL::Translator::Schemas (or SQL files) and produces ALTER
395statments to make them the same
396
397=head1 SNYOPSIS
398
399Simplest usage:
400
401 use SQL::Translator::Diff;
402 my $sql = SQL::Translator::Diff::schema_diff($source_schema, 'MySQL', $target_schema, 'MySQL', $options_hash)
403
404OO usage:
405
406 use SQL::Translator::Diff;
407 my $diff = SQL::Translator::Diff->new({
408 output_db => 'MySQL',
409 source_schema => $source_schema,
410 target_schema => $target_schema,
411 %$options_hash,
412 })->compute_differences->produce_diff_sql;
413
414=head1 OPTIONS
415
416=over
417
418=item B<ignore_index_names>
419
420Match indexes based on types and fields, ignoring name.
421
422=item B<ignore_constraint_names>
423
424Match constrains based on types, fields and tables, ignoring name.
425
426=item B<output_db>
427
428Which producer to use to produce the output.
429
430=item B<case_insensitive>
431
432Ignore case of table, field, index and constraint names when comparing
433
434=item B<no_batch_alters>
435
436Produce each alter as a distinct C<ALTER TABLE> statement even if the producer
437supports the ability to do all alters for a table as one statement.
438
439=item B<ignore_missing_methods>
440
441If the diff would need a method that is missing from the producer, just emit a
442comment showing the method is missing, rather than dieing with an error
443
444=back
445
446=head1 PRODUCER FUNCTIONS
447
448The following producer functions should be implemented for completeness. If
449any of them are needed for a given diff, but not found, an error will be
450thrown.
451
452=over
453
454=item * C<alter_create_constraint($con)>
455
456=item * C<alter_drop_constraint($con)>
457
458=item * C<alter_create_index($idx)>
459
460=item * C<alter_drop_index($idx)>
461
462=item * C<add_field($fld)>
463
464=item * C<alter_field($old_fld, $new_fld)>
465
466=item * C<rename_field($old_fld, $new_fld)>
467
468=item * C<drop_field($fld)>
469
470=item * C<alter_table($table)>
471
472=item * C<drop_table($table)>
473
474=item * C<batch_alter_table($table, $hash)> (optional)
475
476=back
477
478If the producer supports C<batch_alter_table>, it will be called with the
479table to alter and a hash, the keys of which will be the method names listed
480above; values will be arrays of fields or constraints to operate on. In the
481case of the field functions that take two arguments this will appear as a hash.
482
483I.e. the hash might look something like the following:
484
485 {
486 alter_create_constraint => [ $constraint1, $constraint2 ],
487 add_field => [ $field ],
488 alter_field => [ [$old_field, $new_field] ]
489 }
490
491=head1 AUTHOR
492
493Original Author(s) unknown.
494
495Refactor and more comprehensive tests by Ash Berlin C<< ash@cpan.org >>.
496
497Redevelopment sponsored by Takkle Inc.
498
499=cut