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