1 package SQL::Translator::Diff;
2 ## SQLT schema diffing code
5 use SQL::Translator::Schema::Constants;
10 my ($source_schema, $source_db, $target_schema, $target_db, $options) = @_;
11 # print Data::Dumper::Dumper($target_schema);
12 my $caseopt = $options->{caseopt} || 0;
13 my $debug = $options->{debug} || 0;
14 my $trace = $options->{trace} || 0;
16 my $case_insensitive = $source_db =~ /SQLServer/ || $caseopt;
18 my $tar_name = $target_schema->name;
19 my $src_name = $source_schema->name;
20 my ( @new_tables, @diffs , @diffs_at_end);
21 for my $tar_table ( $target_schema->get_tables ) {
22 my $tar_table_name = $tar_table->name;
23 my $src_table = $source_schema->get_table( $tar_table_name, $case_insensitive );
25 warn "TABLE '$tar_name.$tar_table_name'\n" if $debug;
26 unless ( $src_table ) {
27 warn "Couldn't find table '$tar_name.$tar_table_name' in '$src_name'\n"
29 if ( $source_db =~ /(SQLServer|Oracle)/ ) {
30 for my $constraint ( $tar_table->get_constraints ) {
31 next if $constraint->type ne FOREIGN_KEY;
32 push @diffs_at_end, "ALTER TABLE $tar_table_name ADD ".
33 constraint_to_string($constraint, $source_db, $target_schema).";";
34 $tar_table->drop_constraint($constraint);
37 push @new_tables, $tar_table;
41 # Go through our options
42 my $options_different = 0;
45 for my $tar_table_option_ref ( $tar_table->options ) {
46 my($key_tar, $value_tar) = %{$tar_table_option_ref};
47 for my $src_table_option_ref ( $src_table->options ) {
48 my($key_src, $value_src) = %{$src_table_option_ref};
49 if ( $key_tar eq $key_src ) {
50 if ( defined $value_tar != defined $value_src ) {
51 $options_different = 1;
54 if ( defined $value_tar && $value_tar ne $value_src ) {
55 $options_different = 1;
58 $checkedOptions{$key_tar} = 1;
62 $options_different = 1;
65 # Go through the other table's options
66 unless ( $options_different ) {
67 for my $src_table_option_ref ( $src_table->options ) {
68 my($key, $value) = %{$src_table_option_ref};
69 next if $checkedOptions{$key};
70 $options_different = 1;
74 # If there's a difference, just re-set all the options
75 my @diffs_table_options;
76 if ( $options_different ) {
78 foreach my $option_ref ( $tar_table->options ) {
79 my($key, $value) = %{$option_ref};
80 push(@options, defined $value ? "$key=$value" : $key);
82 my $options = join(' ', @options);
83 @diffs_table_options = ("ALTER TABLE $tar_table_name $options;");
86 my $src_table_name = $src_table->name;
87 my(@diffs_table_adds, @diffs_table_changes);
88 for my $tar_table_field ( $tar_table->get_fields ) {
89 my $f_tar_type = $tar_table_field->data_type;
90 my $f_tar_size = $tar_table_field->size;
91 my $f_tar_name = $tar_table_field->name;
92 my $f_tar_nullable = $tar_table_field->is_nullable;
93 my $f_tar_default = $tar_table_field->default_value;
94 my $f_tar_auto_inc = $tar_table_field->is_auto_increment;
95 my $src_table_field = $src_table->get_field( $f_tar_name, $case_insensitive );
96 my $f_tar_full_name = "$tar_name.$tar_table_name.$tar_table_name";
97 warn "FIELD '$f_tar_full_name'\n" if $debug;
99 my $f_src_full_name = "$src_name.$src_table_name.$f_tar_name";
101 unless ( $src_table_field ) {
102 warn "Couldn't find field '$f_src_full_name' in '$src_table_name'\n"
104 my $temp_default_value = 0;
105 if ( $source_db =~ /SQLServer/ &&
107 !defined $f_tar_default ) {
108 # SQL Server doesn't allow adding non-nullable, non-default columns
109 # so we add it with a default value, then remove the default value
110 $temp_default_value = 1;
111 my(@numeric_types) = qw(decimal numeric float real int bigint smallint tinyint);
112 $f_tar_default = grep($_ eq $f_tar_type, @numeric_types) ? 0 : '';
114 push @diffs_table_adds, sprintf
115 ( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
116 $tar_table_name, $source_db =~ /Oracle/ ? '(' : '',
117 $f_tar_name, $f_tar_type,
118 ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
119 !defined $f_tar_default ? ''
120 : uc $f_tar_default eq 'NULL' ? ' DEFAULT NULL'
121 : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
122 : " DEFAULT '$f_tar_default'",
123 $f_tar_nullable ? '' : ' NOT NULL',
124 $f_tar_auto_inc ? ' AUTO_INCREMENT' : '',
125 $source_db =~ /Oracle/ ? ')' : '',
127 if ( $temp_default_value ) {
128 undef $f_tar_default;
129 push @diffs_table_adds, sprintf
131 DECLARE \@defname VARCHAR(100), \@cmd VARCHAR(1000)
134 FROM sysobjects so JOIN sysconstraints sc
135 ON so.id = sc.constid
136 WHERE object_name(so.parent_obj) = '%s'
139 (SELECT colid FROM syscolumns
140 WHERE id = object_id('%s') AND
142 SET \@cmd = 'ALTER TABLE %s DROP CONSTRAINT '
146 , $tar_table_name, $tar_table_name, $f_tar_name, $tar_table_name,
152 my $f_src_type = $src_table_field->data_type;
153 my $f_src_size = $src_table_field->size || '';
154 my $f_src_nullable = $src_table_field->is_nullable;
155 my $f_src_default = $src_table_field->default_value;
156 my $f_src_auto_inc = $src_table_field->is_auto_increment;
157 if ( !$tar_table_field->equals($src_table_field, $case_insensitive) ) {
158 # SQLServer timestamp fields can't be altered, so we drop and add instead
159 if ( $source_db =~ /SQLServer/ && $f_src_type eq "timestamp" ) {
160 push @diffs_table_changes, "ALTER TABLE $tar_table_name DROP COLUMN $f_tar_name;";
161 push @diffs_table_changes, sprintf
162 ( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
163 $tar_table_name, $source_db =~ /Oracle/ ? '(' : '',
164 $f_tar_name, $f_tar_type,
165 ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
166 !defined $f_tar_default ? ''
167 : uc $f_tar_default eq 'NULL' ? ' DEFAULT NULL'
168 : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
169 : " DEFAULT '$f_tar_default'",
170 $f_tar_nullable ? '' : ' NOT NULL',
171 $f_tar_auto_inc ? ' AUTO_INCREMENT' : '',
172 $source_db =~ /Oracle/ ? ')' : '',
177 my $changeText = $source_db =~ /SQLServer/ ? 'ALTER COLUMN' :
178 $source_db =~ /Oracle/ ? 'MODIFY (' : 'CHANGE';
179 my $nullText = $f_tar_nullable ? '' : ' NOT NULL';
180 $nullText = '' if $source_db =~ /Oracle/ && $f_tar_nullable == $f_src_nullable;
181 push @diffs_table_changes, sprintf
182 ( "ALTER TABLE %s %s %s%s %s%s%s%s%s%s;",
183 $tar_table_name, $changeText,
184 $f_tar_name, $source_db =~ /MySQL/ ? " $f_tar_name" : '',
185 $f_tar_type, ($f_tar_size && $f_tar_type !~ /(blob|text)$/) ? "($f_tar_size)" : '',
187 !defined $f_tar_default || $source_db =~ /SQLServer/ ? ''
188 : uc $f_tar_default eq 'NULL' ? ' DEFAULT NULL'
189 : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
190 : " DEFAULT '$f_tar_default'",
191 $f_tar_auto_inc ? ' AUTO_INCREMENT' : '',
192 $source_db =~ /Oracle/ ? ')' : '',
194 if ( defined $f_tar_default && $source_db =~ /SQLServer/ ) {
195 # Adding a column with a default value for SQL Server means adding a
196 # constraint and setting existing NULLs to the default value
197 push @diffs_table_changes, sprintf
198 ( "ALTER TABLE %s ADD CONSTRAINT DF_%s_%s %s FOR %s;",
199 $tar_table_name, $tar_table_name, $f_tar_name, uc $f_tar_default eq 'NULL' ? 'DEFAULT NULL'
200 : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP'
201 : "DEFAULT '$f_tar_default'", $f_tar_name,
203 push @diffs_table_changes, sprintf
204 ( "UPDATE %s SET %s = %s WHERE %s IS NULL;",
205 $tar_table_name, $f_tar_name, uc $f_tar_default eq 'NULL' ? 'NULL'
206 : uc $f_tar_default eq 'CURRENT_TIMESTAMP' ? 'CURRENT_TIMESTAMP'
207 : "'$f_tar_default'", $f_tar_name,
213 my(%checked_indices, @diffs_index_creates, @diffs_index_drops);
215 for my $i_tar ( $tar_table->get_indices ) {
216 for my $i_src ( $src_table->get_indices ) {
217 if ( $i_tar->equals($i_src, $case_insensitive) ) {
218 $checked_indices{$i_src} = 1;
222 push @diffs_index_creates, sprintf
223 ( "CREATE %sINDEX%s ON %s (%s);",
224 $i_tar->type eq NORMAL ? '' : $i_tar->type." ",
225 $i_tar->name ? " ".$i_tar->name : '',
227 join(",", $i_tar->fields),
231 for my $i_src ( $src_table->get_indices ) {
232 next if $checked_indices{$i_src};
233 for my $i_tar ( $tar_table->get_indices ) {
234 next INDEX2 if $i_src->equals($i_tar, $case_insensitive);
236 $source_db =~ /SQLServer/
237 ? push @diffs_index_drops, "DROP INDEX $tar_table_name.".$i_src->name.";"
238 : push @diffs_index_drops, "DROP INDEX ".$i_src->name." on $tar_table_name;";
241 my(%checked_constraints, @diffs_constraint_drops);
243 for my $c_tar ( $tar_table->get_constraints ) {
244 next if $target_db =~ /Oracle/ &&
245 $c_tar->type eq UNIQUE && $c_tar->name =~ /^SYS_/i;
246 for my $c_src ( $src_table->get_constraints ) {
247 if ( $c_tar->equals($c_src, $case_insensitive) ) {
248 $checked_constraints{$c_src} = 1;
252 push @diffs_at_end, "ALTER TABLE $tar_table_name ADD ".
253 constraint_to_string($c_tar, $source_db, $target_schema).";";
256 for my $c_src ( $src_table->get_constraints ) {
257 next if $checked_constraints{$c_src};
258 for my $c_tar ( $tar_table->get_constraints ) {
259 next CONSTRAINT2 if $c_src->equals($c_tar, $case_insensitive);
261 if ( $c_src->type eq UNIQUE ) {
262 push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP INDEX ".
264 } elsif ( $source_db =~ /SQLServer/ ) {
265 push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP ".$c_src->name.";";
267 push @diffs_constraint_drops, "ALTER TABLE $tar_table_name DROP ".$c_src->type.
268 ($c_src->type eq FOREIGN_KEY ? " ".$c_src->name : '').";";
272 push @diffs, @diffs_index_drops, @diffs_constraint_drops,
273 @diffs_table_options, @diffs_table_adds,
274 @diffs_table_changes, @diffs_index_creates;
277 for my $src_table ( $source_schema->get_tables ) {
278 my $src_table_name = $src_table->name;
279 my $tar_table = $target_schema->get_table( $src_table_name, $source_db =~ /SQLServer/ );
281 unless ( $tar_table ) {
282 if ( $source_db =~ /SQLServer/ ) {
283 for my $constraint ( $src_table->get_constraints ) {
284 next if $constraint->type eq PRIMARY_KEY;
285 push @diffs, "ALTER TABLE $src_table_name DROP ".$constraint->name.";";
288 push @diffs_at_end, "DROP TABLE $src_table_name;";
292 for my $src_table_field ( $src_table->get_fields ) {
293 my $f_src_name = $src_table_field->name;
294 my $tar_table_field = $tar_table->get_field( $f_src_name );
295 unless ( $tar_table_field ) {
296 my $modifier = $source_db =~ /SQLServer/ ? "COLUMN " : '';
297 push @diffs, "ALTER TABLE $src_table_name DROP $modifier$f_src_name;";
303 my $dummytr = SQL::Translator->new;
304 $dummytr->schema->add_table( $_ ) for @new_tables;
305 my $producer = $dummytr->producer( $source_db );
306 unshift @diffs, $producer->( $dummytr );
308 push(@diffs, @diffs_at_end);
312 "-- Convert schema '$src_name' to '$tar_name':\n", @diffs, "\n"
318 sub constraint_to_string {
320 my $source_db = shift;
321 my $schema = shift or die "No schema given";
322 my @fields = $c->field_names or return '';
324 if ( $c->type eq PRIMARY_KEY ) {
325 if ( $source_db =~ /Oracle/ ) {
326 return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
327 'PRIMARY KEY (' . join(', ', @fields). ')';
329 return 'PRIMARY KEY (' . join(', ', @fields). ')';
332 elsif ( $c->type eq UNIQUE ) {
333 if ( $source_db =~ /Oracle/ ) {
334 return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
335 'UNIQUE (' . join(', ', @fields). ')';
338 (defined $c->name ? $c->name.' ' : '').
339 '(' . join(', ', @fields). ')';
342 elsif ( $c->type eq FOREIGN_KEY ) {
344 map { $_ || () } 'CONSTRAINT', $c->name, 'FOREIGN KEY'
347 $def .= ' (' . join( ', ', @fields ) . ')';
349 $def .= ' REFERENCES ' . $c->reference_table;
351 my @rfields = map { $_ || () } $c->reference_fields;
352 unless ( @rfields ) {
353 my $rtable_name = $c->reference_table;
354 if ( my $ref_table = $schema->get_table( $rtable_name ) ) {
355 push @rfields, $ref_table->primary_key;
358 warn "Can't find reference table '$rtable_name' " .
364 $def .= ' (' . join( ', ', @rfields ) . ')';
367 warn "FK constraint on " . 'some table' . '.' .
368 join('', @fields) . " has no reference fields\n";
371 if ( $c->match_type ) {
373 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
376 if ( $c->on_delete ) {
377 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
380 if ( $c->on_update ) {
381 $def .= ' ON UPDATE '.join( ' ', $c->on_update );