Commit | Line | Data |
51ffe5ee |
1 | package SQL::Translator::Diff; |
4d438549 |
2 | |
3 | |
51ffe5ee |
4 | ## SQLT schema diffing code |
5 | use strict; |
6 | use warnings; |
4d438549 |
7 | |
da5a1bae |
8 | use Data::Dumper; |
51ffe5ee |
9 | use SQL::Translator::Schema::Constants; |
10 | |
4d438549 |
11 | use 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 | |
20 | my @diff_arrays = qw/ |
21 | tables_to_drop |
22 | tables_to_create |
23 | /; |
24 | |
25 | my @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 | |
39 | sub 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 |
61 | sub 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 | |
70 | sub 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 | |
120 | sub 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 | |
237 | sub 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 | |
263 | sub 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 | |
291 | sub 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 | |
339 | sub 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 |
384 | 1; |
4d438549 |
385 | |
386 | __END__ |
387 | |
388 | =head1 NAME |
389 | |
390 | SQL::Translator::Diff |
391 | |
392 | =head1 DESCRIPTION |
393 | |
394 | Takes two input SQL::Translator::Schemas (or SQL files) and produces ALTER |
395 | statments to make them the same |
396 | |
397 | =head1 SNYOPSIS |
398 | |
399 | Simplest usage: |
400 | |
401 | use SQL::Translator::Diff; |
402 | my $sql = SQL::Translator::Diff::schema_diff($source_schema, 'MySQL', $target_schema, 'MySQL', $options_hash) |
403 | |
404 | OO 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 | |
420 | Match indexes based on types and fields, ignoring name. |
421 | |
422 | =item B<ignore_constraint_names> |
423 | |
424 | Match constrains based on types, fields and tables, ignoring name. |
425 | |
426 | =item B<output_db> |
427 | |
428 | Which producer to use to produce the output. |
429 | |
430 | =item B<case_insensitive> |
431 | |
432 | Ignore case of table, field, index and constraint names when comparing |
433 | |
434 | =item B<no_batch_alters> |
435 | |
436 | Produce each alter as a distinct C<ALTER TABLE> statement even if the producer |
437 | supports the ability to do all alters for a table as one statement. |
438 | |
439 | =item B<ignore_missing_methods> |
440 | |
441 | If the diff would need a method that is missing from the producer, just emit a |
442 | comment showing the method is missing, rather than dieing with an error |
443 | |
444 | =back |
445 | |
446 | =head1 PRODUCER FUNCTIONS |
447 | |
448 | The following producer functions should be implemented for completeness. If |
449 | any of them are needed for a given diff, but not found, an error will be |
450 | thrown. |
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 | |
478 | If the producer supports C<batch_alter_table>, it will be called with the |
479 | table to alter and a hash, the keys of which will be the method names listed |
480 | above; values will be arrays of fields or constraints to operate on. In the |
481 | case of the field functions that take two arguments this will appear as a hash. |
482 | |
483 | I.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 | |
493 | Original Author(s) unknown. |
494 | |
495 | Refactor and more comprehensive tests by Ash Berlin C<< ash@cpan.org >>. |
496 | |
497 | Redevelopment sponsored by Takkle Inc. |
498 | |
499 | =cut |