Added options for ignoring the differences in SQL for views and procedures
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Diff.pm
1 package SQL::Translator::Diff;
2 ## SQLT schema diffing code
3 use strict;
4 use warnings;
5 use SQL::Translator::Schema::Constants;
6
7 sub schema_diff
8 {
9 #  use Data::Dumper;
10     my ($source_schema, $source_db, $target_schema, $target_db, $options) = @_;
11 #  print Data::Dumper::Dumper($target_schema);
12     my $case_insensitive = $options->{caseopt} || 0;
13     my $debug = $options->{debug} || 0;
14     my $trace = $options->{trace} || 0;
15     my $ignore_index_names = $options->{ignore_index_names} || 0;
16     my $ignore_constraint_names = $options->{ignore_constraint_names} || 0;
17     my $ignore_view_sql = $options->{ignore_view_sql} || 0;
18     my $ignore_proc_sql = $options->{ignore_proc_sql} || 0;
19     my $output_db = $options->{output_db} || $source_db;
20
21     my $tar_name  = $target_schema->name;
22     my $src_name  = $source_schema->name;
23     my ( @new_tables, @diffs , @diffs_at_end);
24     for my $tar_table ( $target_schema->get_tables ) {
25         my $tar_table_name = $tar_table->name;
26         my $src_table      = $source_schema->get_table( $tar_table_name, $case_insensitive );
27
28         warn "TABLE '$tar_name.$tar_table_name'\n" if $debug;
29         unless ( $src_table ) {
30             warn "Couldn't find table '$tar_name.$tar_table_name' in '$src_name'\n"
31                 if $debug;
32             if ( $output_db =~ /(SQLServer|Oracle)/ ) {
33                 for my $constraint ( $tar_table->get_constraints ) {
34                     next if $constraint->type ne FOREIGN_KEY;
35                     push @diffs_at_end, "ALTER TABLE $tar_table_name ADD ".
36                         constraint_to_string($constraint, $output_db, $target_schema).";";
37                     $tar_table->drop_constraint($constraint);
38                 }
39             }
40             push @new_tables, $tar_table;
41             next;
42         }
43
44         # Go through our options
45         my $options_different = 0;
46         my %checkedOptions;
47       OPTION:
48         for my $tar_table_option_ref ( $tar_table->options ) {
49             my($key_tar, $value_tar) = %{$tar_table_option_ref};
50             for my $src_table_option_ref ( $src_table->options ) {
51                 my($key_src, $value_src) = %{$src_table_option_ref};
52                 if ( $key_tar eq $key_src ) {
53                     if ( defined $value_tar != defined $value_src ) {
54                         $options_different = 1;
55                         last OPTION;
56                     }
57                     if ( defined $value_tar && $value_tar ne $value_src ) {
58                         $options_different = 1;
59                         last OPTION;
60                     }
61                     $checkedOptions{$key_tar} = 1;
62                     next OPTION;
63                 }
64             }
65             $options_different = 1;
66             last OPTION;
67         }
68         # Go through the other table's options
69         unless ( $options_different ) {
70             for my $src_table_option_ref ( $src_table->options ) {
71                 my($key, $value) = %{$src_table_option_ref};
72                 next if $checkedOptions{$key};
73                 $options_different = 1;
74                 last;
75             }
76         }
77         # If there's a difference, just re-set all the options
78         my @diffs_table_options;
79         if ( $options_different ) {
80             my @options = ();
81             foreach my $option_ref ( $tar_table->options ) {
82                 my($key, $value) = %{$option_ref};
83                 push(@options, defined $value ? "$key=$value" : $key);
84             }
85             my $options = join(' ', @options);
86             @diffs_table_options = ("ALTER TABLE $tar_table_name $options;");
87         }
88
89         my $src_table_name = $src_table->name;
90         my(@diffs_table_adds, @diffs_table_changes);
91         for my $tar_table_field ( $tar_table->get_fields ) {
92             my $f_tar_type      = $tar_table_field->data_type;
93             my $f_tar_size      = $tar_table_field->size;
94             my $f_tar_name      = $tar_table_field->name;
95             my $f_tar_nullable  = $tar_table_field->is_nullable;
96             my $f_tar_default   = $tar_table_field->default_value;
97             my $f_tar_auto_inc  = $tar_table_field->is_auto_increment;
98             my $src_table_field     = $src_table->get_field( $f_tar_name, $case_insensitive );
99             my $f_tar_full_name = "$tar_name.$tar_table_name.$f_tar_name";
100             warn "FIELD '$f_tar_full_name'\n" if $debug;
101
102             my $f_src_full_name = "$src_name.$src_table_name.$f_tar_name";
103
104             unless ( $src_table_field ) {
105                 warn "Couldn't find field '$f_src_full_name' in '$src_table_name'\n" 
106                     if $debug;
107                 my $temp_default_value = 0;
108                 if ( $output_db =~ /SQLServer/ && 
109                      !$f_tar_nullable             && 
110                      !defined $f_tar_default ) {
111                     # SQL Server doesn't allow adding non-nullable, non-default columns
112                     # so we add it with a default value, then remove the default value
113                     $temp_default_value = 1;
114                     my(@numeric_types) = qw(decimal numeric float real int bigint smallint tinyint);
115                     $f_tar_default = grep($_ eq $f_tar_type, @numeric_types) ? 0 : '';
116                 }
117                 push @diffs_table_adds, sprintf
118                     ( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
119                       $tar_table_name, $output_db =~ /Oracle/ ? '(' : '',
120                       $f_tar_name, $f_tar_type,
121                       ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
122                       !defined $f_tar_default ? ''
123                       : uc $f_tar_default eq 'NULL' ? ' DEFAULT NULL'
124                       : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
125                       : " DEFAULT '$f_tar_default'",
126                       $f_tar_nullable ? '' : ' NOT NULL',
127                       $f_tar_auto_inc ? ' AUTO_INCREMENT' : '',
128                       $output_db =~ /Oracle/ ? ')' : '',
129                       );
130                 if ( $temp_default_value ) {
131                     undef $f_tar_default;
132                     push @diffs_table_adds, sprintf
133                         ( <<END
134 DECLARE \@defname VARCHAR(100), \@cmd VARCHAR(1000)
135 SET \@defname = 
136 (SELECT name 
137  FROM sysobjects so JOIN sysconstraints sc
138   ON so.id = sc.constid 
139  WHERE object_name(so.parent_obj) = '%s' 
140    AND so.xtype = 'D'
141    AND sc.colid = 
142     (SELECT colid FROM syscolumns 
143      WHERE id = object_id('%s') AND 
144          name = '%s'))
145 SET \@cmd = 'ALTER TABLE %s DROP CONSTRAINT '
146 + \@defname
147 EXEC(\@cmd)
148 END
149                          , $tar_table_name, $tar_table_name, $f_tar_name, $tar_table_name,
150                         );
151                   }
152                 next;
153               }
154
155             my $f_src_type = $src_table_field->data_type;
156             my $f_src_size = $src_table_field->size || '';
157             my $f_src_nullable  = $src_table_field->is_nullable;
158             my $f_src_default   = $src_table_field->default_value;
159             my $f_src_auto_inc  = $src_table_field->is_auto_increment;
160             if ( !$tar_table_field->equals($src_table_field, $case_insensitive) ) {
161               # SQLServer timestamp fields can't be altered, so we drop and add instead
162               if ( $output_db =~ /SQLServer/ && $f_src_type eq "timestamp" ) {
163                         push @diffs_table_changes, "ALTER TABLE $tar_table_name DROP COLUMN $f_tar_name;";
164                     push @diffs_table_changes, sprintf
165                   ( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
166                     $tar_table_name, $output_db =~ /Oracle/ ? '(' : '',
167                     $f_tar_name, $f_tar_type,
168                     ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
169                     !defined $f_tar_default ? ''
170                     : uc $f_tar_default eq 'NULL' ? ' DEFAULT NULL'
171                     : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
172                     : " DEFAULT '$f_tar_default'",
173                         $f_tar_nullable ? '' : ' NOT NULL',
174                         $f_tar_auto_inc ? ' AUTO_INCREMENT' : '',
175                         $output_db =~ /Oracle/ ? ')' : '',
176                   );
177                     next;
178               }
179
180               my $changeText = $output_db =~ /SQLServer/ ? 'ALTER COLUMN' :
181                                 $output_db =~ /Oracle/ ? 'MODIFY (' : 'CHANGE';
182               my $nullText = $f_tar_nullable ? '' : ' NOT NULL';
183               $nullText = '' if $output_db =~ /Oracle/ && $f_tar_nullable == $f_src_nullable;
184               push @diffs_table_changes, sprintf
185                 ( "ALTER TABLE %s %s %s%s %s%s%s%s%s%s;",
186                   $tar_table_name, $changeText,
187                   $f_tar_name, $output_db =~ /MySQL/ ? " $f_tar_name" : '',
188                   $f_tar_type, ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
189                   $nullText,
190                   !defined $f_tar_default || $output_db =~ /SQLServer/ ? ''
191                   : uc $f_tar_default eq 'NULL' ? ' DEFAULT NULL'
192                   : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
193                   : " DEFAULT '$f_tar_default'",
194                   $f_tar_auto_inc ? ' AUTO_INCREMENT' : '',
195                   $output_db =~ /Oracle/ ? ')' : '',
196                 );
197               if ( defined $f_tar_default && $output_db =~ /SQLServer/ ) {
198                 # Adding a column with a default value for SQL Server means adding a 
199                 # constraint and setting existing NULLs to the default value
200                 push @diffs_table_changes, sprintf
201                   ( "ALTER TABLE %s ADD CONSTRAINT DF_%s_%s %s FOR %s;",
202                     $tar_table_name, $tar_table_name, $f_tar_name, uc $f_tar_default eq 'NULL' ? 'DEFAULT NULL'
203                     : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP'
204                         : "DEFAULT '$f_tar_default'", $f_tar_name,
205                   );
206                 push @diffs_table_changes, sprintf
207                   ( "UPDATE %s SET %s = %s WHERE %s IS NULL;",
208                     $tar_table_name, $f_tar_name, uc $f_tar_default eq 'NULL' ? 'NULL'
209                         : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? 'CURRENT_TIMESTAMP'
210                         : "'$f_tar_default'", $f_tar_name,
211                   );
212               }
213             }
214           }
215
216         my(%checked_indices, @diffs_index_creates, @diffs_index_drops);
217       INDEX:
218         for my $i_tar ( $tar_table->get_indices ) {
219           for my $i_src ( $src_table->get_indices ) {
220                         if ( $i_tar->equals($i_src, $case_insensitive, $ignore_index_names) ) {
221               $checked_indices{$i_src} = 1;
222               next INDEX;
223                         }
224           }
225           push @diffs_index_creates, sprintf
226             ( "CREATE %sINDEX%s ON %s (%s);",
227               $i_tar->type eq NORMAL ? '' : $i_tar->type." ",
228               $i_tar->name ? " ".$i_tar->name : '',
229               $tar_table_name,
230               join(",", $i_tar->fields),
231             );
232         }
233       INDEX2:
234         for my $i_src ( $src_table->get_indices ) {
235           next if !$ignore_index_names && $checked_indices{$i_src};
236           for my $i_tar ( $tar_table->get_indices ) {
237                         next INDEX2 if $i_src->equals($i_tar, $case_insensitive, $ignore_index_names);
238           }
239           $output_db =~ /SQLServer/
240                         ? push @diffs_index_drops, "DROP INDEX $tar_table_name.".$i_src->name.";"
241               : push @diffs_index_drops, "DROP INDEX ".$i_src->name." on $tar_table_name;";
242         }
243
244         my(%checked_constraints, @diffs_constraint_drops);
245       CONSTRAINT:
246         for my $c_tar ( $tar_table->get_constraints ) {
247           for my $c_src ( $src_table->get_constraints ) {
248                         if ( $c_tar->equals($c_src, $case_insensitive, $ignore_constraint_names) ) {
249               $checked_constraints{$c_src} = 1;
250               next CONSTRAINT;
251                         }
252           }
253           push @diffs_at_end, "ALTER TABLE $tar_table_name ADD ".
254                         constraint_to_string($c_tar, $output_db, $target_schema).";";
255         }
256       CONSTRAINT2:
257         for my $c_src ( $src_table->get_constraints ) {
258           next if !$ignore_constraint_names && $checked_constraints{$c_src};
259           for my $c_tar ( $tar_table->get_constraints ) {
260                         next CONSTRAINT2 if $c_src->equals($c_tar, $case_insensitive, $ignore_constraint_names);
261           }
262           if ( $c_src->type eq UNIQUE ) {
263                         push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP INDEX ".
264               $c_src->name.";";
265           } elsif ( $output_db =~ /SQLServer/ ) {
266                         push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP ".$c_src->name.";";
267           } else {
268                         push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP ".$c_src->type.
269               ($c_src->type eq FOREIGN_KEY ? " ".$c_src->name : '').";";
270           }
271         }
272
273         push @diffs, @diffs_index_drops, @diffs_constraint_drops,
274           @diffs_table_options, @diffs_table_adds,
275             @diffs_table_changes, @diffs_index_creates;
276       }
277
278     for my $src_table ( $source_schema->get_tables ) {
279       my $src_table_name = $src_table->name;
280       my $tar_table      = $target_schema->get_table( $src_table_name, $case_insensitive );
281
282       unless ( $tar_table ) {
283         if ( $output_db =~ /SQLServer/ ) {
284           for my $constraint ( $src_table->get_constraints ) {
285             next if $constraint->type eq PRIMARY_KEY;
286             push @diffs, "ALTER TABLE $src_table_name DROP ".$constraint->name.";";
287           }
288         }
289         push @diffs_at_end, "DROP TABLE $src_table_name;";
290         next;
291       }
292
293       for my $src_table_field ( $src_table->get_fields ) {
294         my $f_src_name      = $src_table_field->name;
295         my $tar_table_field     = $tar_table->get_field( $f_src_name, $case_insensitive );
296         unless ( $tar_table_field ) {
297           my $modifier = $output_db =~ /SQLServer/ ? "COLUMN " : '';
298           push @diffs, "ALTER TABLE $src_table_name DROP $modifier$f_src_name;";
299         }
300       }
301     }
302
303     if ( @new_tables ) {
304       my $dummytr = SQL::Translator->new;
305       $dummytr->schema->add_table( $_ ) for @new_tables;
306       my $producer = $dummytr->producer( $output_db );
307       unshift @diffs, $producer->( $dummytr );
308     }
309     push(@diffs, @diffs_at_end);
310
311         # Procedures
312     my(%checked_procs, @diffs_proc_creates, @diffs_proc_drops);
313   PROC:
314     for my $p_tar ( $target_schema->get_procedures ) {
315       for my $p_src ( $source_schema->get_procedures ) {
316                 if ( $p_tar->equals($p_src, $case_insensitive, $ignore_proc_sql) ) {
317           $checked_procs{$p_src} = 1;
318           next PROC;
319                 }
320       }
321       push @diffs_proc_creates, $p_tar->sql;
322     }
323   PROC2:
324     for my $p_src ( $source_schema->get_procedures ) {
325       next if $checked_procs{$p_src};
326       for my $p_tar ( $target_schema->get_procedures ) {
327                 next PROC2 if $p_src->equals($p_tar, $case_insensitive, $ignore_proc_sql);
328       }
329       my $proc_ident = $p_src->owner ? sprintf("[%s].%s", $p_src->owner, $p_src->name) : $p_src->name;
330       push @diffs_proc_drops, "DROP PROCEDURE $proc_ident;\nGO\n";
331     }
332
333         # Views
334     my(%checked_views, @diffs_view_creates, @diffs_view_drops);
335   VIEW:
336     for my $v_tar ( $target_schema->get_views ) {
337       for my $v_src ( $source_schema->get_views ) {
338                 if ( $v_tar->equals($v_src, $case_insensitive, $ignore_view_sql) ) {
339           $checked_views{$v_src} = 1;
340           next VIEW;
341                 }
342       }
343       push @diffs_view_creates, $v_tar->sql;
344     }
345   VIEW2:
346     for my $v_src ( $source_schema->get_views ) {
347       next if $checked_views{$v_src};
348       for my $v_tar ( $target_schema->get_views ) {
349                 next VIEW2 if $v_src->equals($v_tar, $case_insensitive, $ignore_view_sql);
350       }
351       my $view_ident = $v_src->name;
352       push @diffs_view_drops, "DROP VIEW $view_ident;\nGO\n";
353     }
354
355     push @diffs, @diffs_view_drops, @diffs_proc_drops,
356       @diffs_view_creates, @diffs_proc_creates;
357
358     if ( @diffs ) {
359         if ( $target_db !~ /^(MySQL|SQLServer|Oracle)$/ ) {
360                 unshift(@diffs, "-- Target database $target_db is untested/unsupported!!!");
361         }
362         return join( "\n", 
363                   "-- Convert schema '$src_name' to '$tar_name':\n", @diffs, "\n"
364                 );
365     }
366     return undef;
367 }
368
369 sub constraint_to_string {
370   my $c = shift;
371   my $source_db = shift;
372   my $schema = shift or die "No schema given";
373   my @fields = $c->field_names or return '';
374
375   if ( $c->type eq PRIMARY_KEY ) {
376     if ( $source_db =~ /Oracle/ ) {
377       return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
378         'PRIMARY KEY (' . join(', ', @fields). ')';
379     } else {
380       return 'PRIMARY KEY (' . join(', ', @fields). ')';
381     }
382   }
383   elsif ( $c->type eq UNIQUE ) {
384     if ( $source_db =~ /Oracle/ ) {
385       return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
386         'UNIQUE (' . join(', ', @fields). ')';
387     } else {
388       return 'UNIQUE '.
389         (defined $c->name ? $c->name.' ' : '').
390           '(' . join(', ', @fields). ')';
391     }
392   }
393   elsif ( $c->type eq FOREIGN_KEY ) {
394     my $def = join(' ', 
395                    map { $_ || () } 'CONSTRAINT', $c->name, 'FOREIGN KEY' 
396                   );
397
398     $def .= ' (' . join( ', ', @fields ) . ')';
399
400     $def .= ' REFERENCES ' . $c->reference_table;
401
402     my @rfields = map { $_ || () } $c->reference_fields;
403     unless ( @rfields ) {
404       my $rtable_name = $c->reference_table;
405       if ( my $ref_table = $schema->get_table( $rtable_name ) ) {
406         push @rfields, $ref_table->primary_key;
407       }
408       else {
409         warn "Can't find reference table '$rtable_name' " .
410           "in schema\n";
411       }
412     }
413
414     if ( @rfields ) {
415       $def .= ' (' . join( ', ', @rfields ) . ')';
416     }
417     else {
418       warn "FK constraint on " . 'some table' . '.' .
419         join('', @fields) . " has no reference fields\n";
420     }
421
422     if ( $c->match_type ) {
423       $def .= ' MATCH ' . 
424         ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
425     }
426
427     if ( $c->on_delete ) {
428       $def .= ' ON DELETE '.join( ' ', $c->on_delete );
429     }
430
431     if ( $c->on_update ) {
432       $def .= ' ON UPDATE '.join( ' ', $c->on_update );
433     }
434
435     return $def;
436   }
437 }
438
439 1;