Added options for ignoring the differences in SQL for views and procedures
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Diff.pm
CommitLineData
51ffe5ee 1package SQL::Translator::Diff;
2## SQLT schema diffing code
3use strict;
4use warnings;
5use SQL::Translator::Schema::Constants;
6
7sub 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);
8b5b4c99 12 my $case_insensitive = $options->{caseopt} || 0;
51ffe5ee 13 my $debug = $options->{debug} || 0;
14 my $trace = $options->{trace} || 0;
d990d84b 15 my $ignore_index_names = $options->{ignore_index_names} || 0;
16 my $ignore_constraint_names = $options->{ignore_constraint_names} || 0;
d1a895ce 17 my $ignore_view_sql = $options->{ignore_view_sql} || 0;
18 my $ignore_proc_sql = $options->{ignore_proc_sql} || 0;
8b5b4c99 19 my $output_db = $options->{output_db} || $source_db;
51ffe5ee 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;
8b5b4c99 32 if ( $output_db =~ /(SQLServer|Oracle)/ ) {
51ffe5ee 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 ".
8b5b4c99 36 constraint_to_string($constraint, $output_db, $target_schema).";";
51ffe5ee 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 );
8b5b4c99 99 my $f_tar_full_name = "$tar_name.$tar_table_name.$f_tar_name";
51ffe5ee 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;
8b5b4c99 108 if ( $output_db =~ /SQLServer/ &&
51ffe5ee 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;",
8b5b4c99 119 $tar_table_name, $output_db =~ /Oracle/ ? '(' : '',
51ffe5ee 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' : '',
8b5b4c99 128 $output_db =~ /Oracle/ ? ')' : '',
51ffe5ee 129 );
130 if ( $temp_default_value ) {
131 undef $f_tar_default;
132 push @diffs_table_adds, sprintf
133 ( <<END
134DECLARE \@defname VARCHAR(100), \@cmd VARCHAR(1000)
135SET \@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'))
145SET \@cmd = 'ALTER TABLE %s DROP CONSTRAINT '
146+ \@defname
147EXEC(\@cmd)
148END
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
8b5b4c99 162 if ( $output_db =~ /SQLServer/ && $f_src_type eq "timestamp" ) {
51ffe5ee 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;",
8b5b4c99 166 $tar_table_name, $output_db =~ /Oracle/ ? '(' : '',
51ffe5ee 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' : '',
8b5b4c99 175 $output_db =~ /Oracle/ ? ')' : '',
51ffe5ee 176 );
177 next;
178 }
179
8b5b4c99 180 my $changeText = $output_db =~ /SQLServer/ ? 'ALTER COLUMN' :
181 $output_db =~ /Oracle/ ? 'MODIFY (' : 'CHANGE';
51ffe5ee 182 my $nullText = $f_tar_nullable ? '' : ' NOT NULL';
8b5b4c99 183 $nullText = '' if $output_db =~ /Oracle/ && $f_tar_nullable == $f_src_nullable;
51ffe5ee 184 push @diffs_table_changes, sprintf
185 ( "ALTER TABLE %s %s %s%s %s%s%s%s%s%s;",
186 $tar_table_name, $changeText,
8b5b4c99 187 $f_tar_name, $output_db =~ /MySQL/ ? " $f_tar_name" : '',
51ffe5ee 188 $f_tar_type, ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
189 $nullText,
8b5b4c99 190 !defined $f_tar_default || $output_db =~ /SQLServer/ ? ''
51ffe5ee 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' : '',
8b5b4c99 195 $output_db =~ /Oracle/ ? ')' : '',
51ffe5ee 196 );
8b5b4c99 197 if ( defined $f_tar_default && $output_db =~ /SQLServer/ ) {
51ffe5ee 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 ) {
d990d84b 220 if ( $i_tar->equals($i_src, $case_insensitive, $ignore_index_names) ) {
51ffe5ee 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 ) {
03739eb3 235 next if !$ignore_index_names && $checked_indices{$i_src};
51ffe5ee 236 for my $i_tar ( $tar_table->get_indices ) {
d990d84b 237 next INDEX2 if $i_src->equals($i_tar, $case_insensitive, $ignore_index_names);
51ffe5ee 238 }
8b5b4c99 239 $output_db =~ /SQLServer/
51ffe5ee 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 ) {
51ffe5ee 247 for my $c_src ( $src_table->get_constraints ) {
d990d84b 248 if ( $c_tar->equals($c_src, $case_insensitive, $ignore_constraint_names) ) {
51ffe5ee 249 $checked_constraints{$c_src} = 1;
250 next CONSTRAINT;
251 }
252 }
253 push @diffs_at_end, "ALTER TABLE $tar_table_name ADD ".
8b5b4c99 254 constraint_to_string($c_tar, $output_db, $target_schema).";";
51ffe5ee 255 }
256 CONSTRAINT2:
257 for my $c_src ( $src_table->get_constraints ) {
03739eb3 258 next if !$ignore_constraint_names && $checked_constraints{$c_src};
51ffe5ee 259 for my $c_tar ( $tar_table->get_constraints ) {
d990d84b 260 next CONSTRAINT2 if $c_src->equals($c_tar, $case_insensitive, $ignore_constraint_names);
51ffe5ee 261 }
262 if ( $c_src->type eq UNIQUE ) {
263 push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP INDEX ".
264 $c_src->name.";";
8b5b4c99 265 } elsif ( $output_db =~ /SQLServer/ ) {
51ffe5ee 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;
8b5b4c99 280 my $tar_table = $target_schema->get_table( $src_table_name, $case_insensitive );
51ffe5ee 281
282 unless ( $tar_table ) {
8b5b4c99 283 if ( $output_db =~ /SQLServer/ ) {
51ffe5ee 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;
8b5b4c99 295 my $tar_table_field = $tar_table->get_field( $f_src_name, $case_insensitive );
51ffe5ee 296 unless ( $tar_table_field ) {
8b5b4c99 297 my $modifier = $output_db =~ /SQLServer/ ? "COLUMN " : '';
51ffe5ee 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;
8b5b4c99 306 my $producer = $dummytr->producer( $output_db );
51ffe5ee 307 unshift @diffs, $producer->( $dummytr );
308 }
309 push(@diffs, @diffs_at_end);
310
7ac784ff 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 ) {
d1a895ce 316 if ( $p_tar->equals($p_src, $case_insensitive, $ignore_proc_sql) ) {
7ac784ff 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 ) {
d1a895ce 327 next PROC2 if $p_src->equals($p_tar, $case_insensitive, $ignore_proc_sql);
7ac784ff 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 ) {
d1a895ce 338 if ( $v_tar->equals($v_src, $case_insensitive, $ignore_view_sql) ) {
7ac784ff 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 ) {
d1a895ce 349 next VIEW2 if $v_src->equals($v_tar, $case_insensitive, $ignore_view_sql);
7ac784ff 350 }
351 my $view_ident = $v_src->name;
352 push @diffs_view_drops, "DROP VIEW $view_ident;\nGO\n";
353 }
354
ac0cd217 355 push @diffs, @diffs_view_drops, @diffs_proc_drops,
356 @diffs_view_creates, @diffs_proc_creates;
7ac784ff 357
51ffe5ee 358 if ( @diffs ) {
bf459ae6 359 if ( $target_db !~ /^(MySQL|SQLServer|Oracle)$/ ) {
360 unshift(@diffs, "-- Target database $target_db is untested/unsupported!!!");
361 }
51ffe5ee 362 return join( "\n",
363 "-- Convert schema '$src_name' to '$tar_name':\n", @diffs, "\n"
364 );
365 }
366 return undef;
367}
368
369sub 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
4391;