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