0.09000 not 0.0900 (cpan gets confused if numbers go downwards)
[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')) {
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
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
4d438549 485If the producer supports C<batch_alter_table>, it will be called with the
486table to alter and a hash, the keys of which will be the method names listed
487above; values will be arrays of fields or constraints to operate on. In the
488case of the field functions that take two arguments this will appear as a hash.
489
490I.e. the hash might look something like the following:
491
492 {
493 alter_create_constraint => [ $constraint1, $constraint2 ],
494 add_field => [ $field ],
495 alter_field => [ [$old_field, $new_field] ]
496 }
497
934e1b39 498
499=item * C<preprocess_schema($class, $schema)> (optional)
500
501C<preprocess_schema> is called by the Diff code to allow the producer to
502normalize any data it needs to first. For example, the MySQL producer uses
503this method to ensure that FK contraint names are unique.
504
505Basicaly any changes that need to be made to produce the SQL file for the
506schema should be done here, so that a diff between a parsed SQL file and (say)
507a parsed DBIx::Class::Schema object will be sane.
508
509(As an aside, DBIx::Class, for instance, uses the presence of a
510C<preprocess_schema> function on the producer to know that it can diff between
511the previous SQL file and its own internal representation. Without this method
512on th producer it will diff the two SQL files which is slower, but known to
513work better on old-style producers.)
514
515=back
516
517
4d438549 518=head1 AUTHOR
519
520Original Author(s) unknown.
521
934e1b39 522Refactor/re-write and more comprehensive tests by Ash Berlin C<< ash@cpan.org >>.
4d438549 523
524Redevelopment sponsored by Takkle Inc.
525
526=cut