Reduce $Id to its normal form
[dbsrgits/SQL-Translator.git] / bin / sqlt-diff-old
CommitLineData
da5a1bae 1#!/usr/bin/perl -w
2# vim: set ft=perl:
3
4# -------------------------------------------------------------------
782b5a43 5# $Id$
da5a1bae 6# -------------------------------------------------------------------
478f608d 7# Copyright (C) 2002-2009 The SQLFairy Authors
da5a1bae 8#
9# This program is free software; you can redistribute it and/or
10# modify it under the terms of the GNU General Public License as
11# published by the Free Software Foundation; version 2.
12#
13# This program is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16# General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21# 02111-1307 USA
22# -------------------------------------------------------------------
23
24=head1 NAME
25
26sqlt-diff - find the differences b/w two schemas
27
28=head1 SYNOPSIS
29
30For help:
31
32 sqlt-diff -h|--help
33
34For a list of all valid parsers:
35
36 sqlt -l|--list
37
38To diff two schemas:
39
40 sqlt-diff [options] file_name1=parser file_name2=parser
41
42Options:
43
44 -d|--debug Show debugging info
45
46=head1 DESCRIPTION
47
48sqlt-diff is a utility for creating a file of SQL commands necessary to
49transform the first schema provided to the second. While not yet
50exhaustive in its ability to mutate the entire schema, it will report the
51following
52
53=over
54
55=item * New tables
56
57Using the Producer class of the target (second) schema, any tables missing
58in the first schema will be generated in their entirety (fields, constraints,
59indices).
60
61=item * Missing/altered fields
62
63Any fields missing or altered between the two schemas will be reported
64as:
65
66 ALTER TABLE <table_name>
67 [DROP <field_name>]
68 [CHANGE <field_name> <datatype> (<size>)] ;
69
70=item * Missing/altered indices
71
72Any indices missing or of a different type or on different fields will be
73indicated. Indices that should be dropped will be reported as such:
74
75 DROP INDEX <index_name> ON <table_name> ;
76
77An index of a different type or on different fields will be reported as a
78new index as such:
79
80 CREATE [<index_type>] INDEX [<index_name>] ON <table_name>
81 ( <field_name>[,<field_name>] ) ;
82
83=back
84
85"ALTER/DROP TABLE" and "CREATE INDEX" statements B<are not> generated by
86the Producer, unfortunately, and may require massaging before being passed to
87your target database.
88
89=cut
90
91# -------------------------------------------------------------------
92
93use strict;
94use Pod::Usage;
95use Data::Dumper;
96use SQL::Translator;
97use SQL::Translator::Schema::Constants;
98
da06ac74 99use vars qw( $VERSION );
100$VERSION = '1.99';
101
da5a1bae 102my ( @input, $list, $help, $debug );
103for my $arg ( @ARGV ) {
104 if ( $arg =~ m/^-?-l(ist)?$/ ) {
105 $list = 1;
106 }
107 elsif ( $arg =~ m/^-?-h(elp)?$/ ) {
108 $help = 1;
109 }
110 elsif ( $arg =~ m/^-?-d(ebug)?$/ ) {
111 $debug = 1;
112 }
113 elsif ( $arg =~ m/^([^=]+)=(.+)$/ ) {
114 push @input, { file => $1, parser => $2 };
115 }
116 else {
117 pod2usage( msg => "Unknown argument '$arg'" );
118 }
119}
120
121pod2usage(1) if $help;
122pod2usage('Please specify only two schemas to diff') if scalar @input > 2;
123
124my $tr = SQL::Translator->new;
125my @parsers = $tr->list_parsers;
126my %valid_parsers = map { $_, 1 } @parsers;
127
128if ( $list ) {
129 print "\nParsers:\n", map { "\t$_\n" } sort @parsers;
130 print "\n";
131 exit(0);
132}
133
134pod2usage( msg => 'Too many file args' ) if @input > 2;
135
136my ( $source_schema, $source_db, $target_schema, $target_db );
137
138my $i = 2;
139for my $in ( @input ) {
140 my $file = $in->{'file'};
141 my $parser = $in->{'parser'};
142
143 die "Unable to read file '$file'\n" unless -r $file;
144 die "'$parser' is an invalid parser\n" unless $valid_parsers{ $parser };
145
146 my $t = SQL::Translator->new;
147 $t->debug( $debug );
148 $t->parser( $parser ) or die $tr->error;
149 my $out = $t->translate( $file ) or die $tr->error;
150 my $schema = $t->schema;
151 unless ( $schema->name ) {
152 $schema->name( $file );
153 }
154
155 if ( $i == 1 ) {
156 $source_schema = $schema;
157 $source_db = $parser;
158 }
159 else {
160 $target_schema = $schema;
161 $target_db = $parser;
162 }
163 $i--;
164}
165my $case_insensitive = $target_db =~ /SQLServer/;
166
167my $s1_name = $source_schema->name;
168my $s2_name = $target_schema->name;
169my ( @new_tables, @diffs , @diffs_at_end);
170for my $t1 ( $source_schema->get_tables ) {
171 my $t1_name = $t1->name;
172 my $t2 = $target_schema->get_table( $t1_name, $case_insensitive );
173
174 warn "TABLE '$s1_name.$t1_name'\n" if $debug;
175 unless ( $t2 ) {
176 warn "Couldn't find table '$s1_name.$t1_name' in '$s2_name'\n"
177 if $debug;
178 if ( $target_db =~ /(SQLServer|Oracle)/ ) {
179 for my $constraint ( $t1->get_constraints ) {
180 next if $constraint->type ne FOREIGN_KEY;
181 push @diffs_at_end, "ALTER TABLE $t1_name ADD ".
182 constraint_to_string($constraint, $source_schema).";";
183 $t1->drop_constraint($constraint);
184 }
185 }
186 push @new_tables, $t1;
187 next;
188 }
189
190 # Go through our options
191 my $options_different = 0;
192 my %checkedOptions;
193OPTION:
194 for my $t1_option_ref ( $t1->options ) {
195 my($key1, $value1) = %{$t1_option_ref};
196 for my $t2_option_ref ( $t2->options ) {
197 my($key2, $value2) = %{$t2_option_ref};
198 if ( $key1 eq $key2 ) {
199 if ( defined $value1 != defined $value2 ) {
200 $options_different = 1;
201 last OPTION;
202 }
203 if ( defined $value1 && $value1 ne $value2 ) {
204 $options_different = 1;
205 last OPTION;
206 }
207 $checkedOptions{$key1} = 1;
208 next OPTION;
209 }
210 }
211 $options_different = 1;
212 last OPTION;
213 }
214 # Go through the other table's options
215 unless ( $options_different ) {
216 for my $t2_option_ref ( $t2->options ) {
217 my($key, $value) = %{$t2_option_ref};
218 next if $checkedOptions{$key};
219 $options_different = 1;
220 last;
221 }
222 }
223 # If there's a difference, just re-set all the options
224 my @diffs_table_options;
225 if ( $options_different ) {
226 my @options = ();
227 foreach my $option_ref ( $t1->options ) {
228 my($key, $value) = %{$option_ref};
229 push(@options, defined $value ? "$key=$value" : $key);
230 }
231 my $options = join(' ', @options);
232 @diffs_table_options = ("ALTER TABLE $t1_name $options;");
233 }
234
235 my $t2_name = $t2->name;
236 my(@diffs_table_adds, @diffs_table_changes);
237 for my $t1_field ( $t1->get_fields ) {
238 my $f1_type = $t1_field->data_type;
239 my $f1_size = $t1_field->size;
240 my $f1_name = $t1_field->name;
241 my $f1_nullable = $t1_field->is_nullable;
242 my $f1_default = $t1_field->default_value;
243 my $f1_auto_inc = $t1_field->is_auto_increment;
244 my $t2_field = $t2->get_field( $f1_name, $case_insensitive );
245 my $f1_full_name = "$s1_name.$t1_name.$t1_name";
246 warn "FIELD '$f1_full_name'\n" if $debug;
247
248 my $f2_full_name = "$s2_name.$t2_name.$f1_name";
249
250 unless ( $t2_field ) {
251 warn "Couldn't find field '$f2_full_name' in '$t2_name'\n"
252 if $debug;
253 my $temp_default_value = 0;
254 if ( $target_db =~ /SQLServer/ && !$f1_nullable && !defined $f1_default ) {
255 # SQL Server doesn't allow adding non-nullable, non-default columns
256 # so we add it with a default value, then remove the default value
257 $temp_default_value = 1;
258 my(@numeric_types) = qw(decimal numeric float real int bigint smallint tinyint);
259 $f1_default = grep($_ eq $f1_type, @numeric_types) ? 0 : '';
260 }
261 push @diffs_table_adds, sprintf( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
262 $t1_name, $target_db =~ /Oracle/ ? '(' : '',
263 $f1_name, $f1_type,
264 ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
265 !defined $f1_default ? ''
266 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
267 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
268 : " DEFAULT '$f1_default'",
269 $f1_nullable ? '' : ' NOT NULL',
270 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
271 $target_db =~ /Oracle/ ? ')' : '',
272 );
273 if ( $temp_default_value ) {
274 undef $f1_default;
275 push @diffs_table_adds, sprintf( <<END
276DECLARE \@defname VARCHAR(100), \@cmd VARCHAR(1000)
277SET \@defname =
278(SELECT name
279FROM sysobjects so JOIN sysconstraints sc
280ON so.id = sc.constid
281WHERE object_name(so.parent_obj) = '%s'
282AND so.xtype = 'D'
283AND sc.colid =
284 (SELECT colid FROM syscolumns
285 WHERE id = object_id('%s') AND
286 name = '%s'))
287SET \@cmd = 'ALTER TABLE %s DROP CONSTRAINT '
288+ \@defname
289EXEC(\@cmd)
290END
291 , $t1_name, $t1_name, $f1_name, $t1_name,
292 );
293 }
294 next;
295 }
296
297 my $f2_type = $t2_field->data_type;
298 my $f2_size = $t2_field->size || '';
299 my $f2_nullable = $t2_field->is_nullable;
300 my $f2_default = $t2_field->default_value;
301 my $f2_auto_inc = $t2_field->is_auto_increment;
302 if ( !$t1_field->equals($t2_field, $case_insensitive) ) {
303 # SQLServer timestamp fields can't be altered, so we drop and add instead
304 if ( $target_db =~ /SQLServer/ && $f2_type eq "timestamp" ) {
305 push @diffs_table_changes, "ALTER TABLE $t1_name DROP COLUMN $f1_name;";
306 push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD %s%s %s%s%s%s%s%s;",
307 $t1_name, $target_db =~ /Oracle/ ? '(' : '',
308 $f1_name, $f1_type,
309 ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
310 !defined $f1_default ? ''
311 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
312 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
313 : " DEFAULT '$f1_default'",
314 $f1_nullable ? '' : ' NOT NULL',
315 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
316 $target_db =~ /Oracle/ ? ')' : '',
317 );
318 next;
319 }
320
321 my $changeText = $target_db =~ /SQLServer/ ? 'ALTER COLUMN' :
322 $target_db =~ /Oracle/ ? 'MODIFY (' : 'CHANGE';
323 my $nullText = $f1_nullable ? '' : ' NOT NULL';
324 $nullText = '' if $target_db =~ /Oracle/ && $f1_nullable == $f2_nullable;
325 push @diffs_table_changes, sprintf( "ALTER TABLE %s %s %s%s %s%s%s%s%s%s;",
326 $t1_name, $changeText,
327 $f1_name, $target_db =~ /MySQL/ ? " $f1_name" : '',
328 $f1_type, ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '',
329 $nullText,
330 !defined $f1_default || $target_db =~ /SQLServer/ ? ''
331 : uc $f1_default eq 'NULL' ? ' DEFAULT NULL'
332 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP'
333 : " DEFAULT '$f1_default'",
334 $f1_auto_inc ? ' AUTO_INCREMENT' : '',
335 $target_db =~ /Oracle/ ? ')' : '',
336 );
337 if ( defined $f1_default && $target_db =~ /SQLServer/ ) {
338 # Adding a column with a default value for SQL Server means adding a
339 # constraint and setting existing NULLs to the default value
340 push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD CONSTRAINT DF_%s_%s %s FOR %s;",
341 $t1_name, $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'DEFAULT NULL'
342 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP'
343 : "DEFAULT '$f1_default'", $f1_name,
344 );
345 push @diffs_table_changes, sprintf( "UPDATE %s SET %s = %s WHERE %s IS NULL;",
346 $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'NULL'
347 : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'CURRENT_TIMESTAMP'
348 : "'$f1_default'", $f1_name,
349 );
350 }
351 }
352 }
353
354 my(%checked_indices, @diffs_index_creates, @diffs_index_drops);
355INDEX:
356 for my $i1 ( $t1->get_indices ) {
357 for my $i2 ( $t2->get_indices ) {
358 if ( $i1->equals($i2, $case_insensitive) ) {
359 $checked_indices{$i2} = 1;
360 next INDEX;
361 }
362 }
363 push @diffs_index_creates, sprintf(
364 "CREATE %sINDEX%s ON %s (%s);",
365 $i1->type eq NORMAL ? '' : $i1->type." ",
366 $i1->name ? " ".$i1->name : '',
367 $t1_name,
368 join(",", $i1->fields),
369 );
370 }
371INDEX2:
372 for my $i2 ( $t2->get_indices ) {
373 next if $checked_indices{$i2};
374 for my $i1 ( $t1->get_indices ) {
375 next INDEX2 if $i2->equals($i1, $case_insensitive);
376 }
377 $target_db =~ /SQLServer/
378 ? push @diffs_index_drops, "DROP INDEX $t1_name.".$i2->name.";"
379 : push @diffs_index_drops, "DROP INDEX ".$i2->name." on $t1_name;";
380 }
381
382 my(%checked_constraints, @diffs_constraint_drops);
383CONSTRAINT:
384 for my $c1 ( $t1->get_constraints ) {
385 next if $source_db =~ /Oracle/ && $c1->type eq UNIQUE && $c1->name =~ /^SYS_/i;
386 for my $c2 ( $t2->get_constraints ) {
387 if ( $c1->equals($c2, $case_insensitive) ) {
388 $checked_constraints{$c2} = 1;
389 next CONSTRAINT;
390 }
391 }
392 push @diffs_at_end, "ALTER TABLE $t1_name ADD ".
393 constraint_to_string($c1, $source_schema).";";
394 }
395CONSTRAINT2:
396 for my $c2 ( $t2->get_constraints ) {
397 next if $checked_constraints{$c2};
398 for my $c1 ( $t1->get_constraints ) {
399 next CONSTRAINT2 if $c2->equals($c1, $case_insensitive);
400 }
401 if ( $c2->type eq UNIQUE ) {
402 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP INDEX ".
403 $c2->name.";";
404 } elsif ( $target_db =~ /SQLServer/ ) {
405 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->name.";";
406 } else {
407 push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->type.
408 ($c2->type eq FOREIGN_KEY ? " ".$c2->name : '').";";
409 }
410 }
411
412 push @diffs, @diffs_index_drops, @diffs_constraint_drops,
413 @diffs_table_options, @diffs_table_adds,
414 @diffs_table_changes, @diffs_index_creates;
415}
416
417for my $t2 ( $target_schema->get_tables ) {
418 my $t2_name = $t2->name;
419 my $t1 = $source_schema->get_table( $t2_name, $target_db =~ /SQLServer/ );
420
421 unless ( $t1 ) {
422 if ( $target_db =~ /SQLServer/ ) {
423 for my $constraint ( $t2->get_constraints ) {
424 next if $constraint->type eq PRIMARY_KEY;
425 push @diffs, "ALTER TABLE $t2_name DROP ".$constraint->name.";";
426 }
427 }
428 push @diffs_at_end, "DROP TABLE $t2_name;";
429 next;
430 }
431
432 for my $t2_field ( $t2->get_fields ) {
433 my $f2_name = $t2_field->name;
434 my $t1_field = $t1->get_field( $f2_name );
435 unless ( $t1_field ) {
436 my $modifier = $target_db =~ /SQLServer/ ? "COLUMN " : '';
437 push @diffs, "ALTER TABLE $t2_name DROP $modifier$f2_name;";
438 }
439 }
440}
441
442if ( @new_tables ) {
443 my $dummy_tr = SQL::Translator->new;
444 $dummy_tr->schema->add_table( $_ ) for @new_tables;
445 my $producer = $dummy_tr->producer( $target_db );
446 unshift @diffs, $producer->( $dummy_tr );
447}
448push(@diffs, @diffs_at_end);
449
450if ( @diffs ) {
451 if ( $source_db !~ /^(MySQL|SQLServer|Oracle)$/ ) {
452 unshift(@diffs, "-- Target database $target_db is untested/unsupported!!!");
453 }
454}
455
456print STDERR "sqlt-diff-old is deprecated, please try and use sqlt-diff, and tell us about any problems or patch SQL::Translator::Diff\n";
457
458if ( @diffs ) {
459 print join( "\n",
460 "-- Convert schema '$s2_name' to '$s1_name':\n", @diffs, "\n"
461 );
462 exit(1);
463}
464else {
465 print "There were no differences.\n";
466}
467
468sub constraint_to_string {
469 my $c = shift;
470 my $schema = shift or die "No schema given";
471 my @fields = $c->field_names or return '';
472
473 if ( $c->type eq PRIMARY_KEY ) {
474 if ( $target_db =~ /Oracle/ ) {
475 return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
476 'PRIMARY KEY (' . join(', ', @fields). ')';
477 } else {
478 return 'PRIMARY KEY (' . join(', ', @fields). ')';
479 }
480 }
481 elsif ( $c->type eq UNIQUE ) {
482 if ( $target_db =~ /Oracle/ ) {
483 return (defined $c->name ? 'CONSTRAINT '.$c->name.' ' : '') .
484 'UNIQUE (' . join(', ', @fields). ')';
485 } else {
486 return 'UNIQUE '.
487 (defined $c->name ? $c->name.' ' : '').
488 '(' . join(', ', @fields). ')';
489 }
490 }
491 elsif ( $c->type eq FOREIGN_KEY ) {
492 my $def = join(' ',
493 map { $_ || () } 'CONSTRAINT', $c->name, 'FOREIGN KEY'
494 );
495
496 $def .= ' (' . join( ', ', @fields ) . ')';
497
498 $def .= ' REFERENCES ' . $c->reference_table;
499
500 my @rfields = map { $_ || () } $c->reference_fields;
501 unless ( @rfields ) {
502 my $rtable_name = $c->reference_table;
503 if ( my $ref_table = $schema->get_table( $rtable_name ) ) {
504 push @rfields, $ref_table->primary_key;
505 }
506 else {
507 warn "Can't find reference table '$rtable_name' " .
508 "in schema\n";
509 }
510 }
511
512 if ( @rfields ) {
513 $def .= ' (' . join( ', ', @rfields ) . ')';
514 }
515 else {
516 warn "FK constraint on " . 'some table' . '.' .
517 join('', @fields) . " has no reference fields\n";
518 }
519
520 if ( $c->match_type ) {
521 $def .= ' MATCH ' .
522 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
523 }
524
525 if ( $c->on_delete ) {
526 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
527 }
528
529 if ( $c->on_update ) {
530 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
531 }
532
533 return $def;
534 }
535}
536
537# -------------------------------------------------------------------
538# Bring out number weight & measure in a year of dearth.
539# William Blake
540# -------------------------------------------------------------------
541
542=pod
543
544=head1 AUTHOR
545
546Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
547
548=head1 SEE ALSO
549
550SQL::Translator, L<http://sqlfairy.sourceforge.net>.
551
552=cut