]>
Commit | Line | Data |
---|---|---|
e4ec7d21 TC |
1 | #!perl -w |
2 | use strict; | |
3 | use lib '../cgi-bin/modules'; | |
4 | use DBI; | |
5 | use BSE::DB; | |
6 | use Getopt::Long; | |
d2473dc2 TC |
7 | use BSE::API qw(bse_init); |
8 | use Cwd; | |
9 | ||
10 | bse_init("../cgi-bin"); | |
e4ec7d21 TC |
11 | |
12 | my $verbose; | |
13 | my $pretend; | |
14 | my $didbackup; | |
dc872a32 TC |
15 | my $input = "mysql.str"; |
16 | my $wanthelp; | |
ac68f742 TC |
17 | my $charset; |
18 | my $collation; | |
e4ec7d21 TC |
19 | |
20 | Getopt::Long::Configure('bundling'); | |
ac68f742 TC |
21 | GetOptions |
22 | ( | |
23 | "v:i", \$verbose, | |
24 | "n", \$pretend, | |
25 | "b", \$didbackup, | |
26 | "i=s", \$input, | |
27 | "c=s", \$charset, | |
28 | "o=s", \$collation, | |
29 | "h", \$wanthelp | |
30 | ); | |
e4ec7d21 | 31 | $verbose = 1 if defined $verbose && $verbose == 0; |
b19047a6 | 32 | $verbose = 0 unless $verbose; |
e4ec7d21 | 33 | |
dc872a32 TC |
34 | help() if $wanthelp; |
35 | ||
e4ec7d21 TC |
36 | if ($didbackup) { |
37 | print "Since you gave the -b option, I assume you made a backup.\n"; | |
38 | } | |
39 | else { | |
40 | my $conf = int(rand(1000))+1; | |
41 | print <<EOS; | |
42 | This tool attempts to add missing tables, columns and indices to your | |
43 | database. | |
44 | ||
45 | It's possible it will mess up. | |
46 | ||
47 | If you haven't made a backup of your database $conf, MAKE ONE NOW. | |
48 | ||
49 | If you have made a backup of your database enter the number in the | |
50 | middle of the previous paragraph. Any other entry will abort. | |
51 | EOS | |
52 | my $entered = <STDIN>; | |
53 | chomp $entered; | |
718a070d | 54 | if ($entered ne $conf) { |
d2473dc2 | 55 | print "Either you didn't backup your data or you didn't read the message.\n"; |
e4ec7d21 TC |
56 | exit; |
57 | } | |
58 | } | |
59 | ||
60 | my $db = BSE::DB->single; | |
61 | ||
62 | UNIVERSAL::isa($db, 'BSE::DB::Mysql') | |
d2473dc2 | 63 | or die "Sorry, this only works for Mysql databases ($db)\n"; |
e4ec7d21 | 64 | |
dc872a32 TC |
65 | open STRUCT, "< $input" |
66 | or die "Cannot open structure file $input: $!\n"; | |
e4ec7d21 TC |
67 | my %tables; |
68 | my $table; | |
69 | while (<STRUCT>) { | |
70 | chomp; | |
d794b180 | 71 | tr/\r//d; |
e4ec7d21 TC |
72 | if (/^Table\s+([^,]+)/) { |
73 | $table = $1; | |
74 | } | |
c19e2794 TC |
75 | elsif (/^Engine (\w+)/) { |
76 | $table or die "Engine before Table"; | |
77 | $tables{$table}{engine} = $1; | |
78 | } | |
dc872a32 | 79 | elsif (/^Column\s+(\w+);([^;]+);(\w*);([^;]*);([^;]*)/) { |
e4ec7d21 TC |
80 | $table or die "Column before Table"; |
81 | push(@{$tables{$table}{cols}}, | |
82 | { | |
83 | field=>$1, | |
84 | type=>$2, | |
85 | null=>$3, | |
86 | default=>$4, | |
87 | extra=>$5, | |
88 | }); | |
89 | } | |
dc872a32 | 90 | elsif (/^Index\s+(\w+);(\d+);\[(\w+(?:;\w+)*)\]/) { |
e4ec7d21 TC |
91 | $tables{$table}{indices}{$1} = |
92 | { | |
93 | name=>$1, | |
94 | unique => $2, | |
dc872a32 | 95 | cols => [ split /;/, $3 ], |
e4ec7d21 TC |
96 | }; |
97 | } | |
98 | else { | |
99 | die "Unknown structure command $_"; | |
100 | } | |
101 | } | |
102 | close STRUCT; | |
103 | ||
ac68f742 TC |
104 | my %coll_map; |
105 | if ($charset) { | |
106 | my @coll = get_result("show collation"); | |
107 | if (@coll) { | |
108 | for my $coll (@coll) { | |
109 | $coll_map{$coll->{collation}} = $coll->{charset}; | |
110 | if (!$collation | |
111 | && $coll->{charset} eq $charset | |
112 | && $coll->{default}) { | |
113 | $collation = $coll->{collation}; | |
114 | } | |
115 | } | |
116 | unless ($collation) { | |
117 | die "Cannot find a default collation for charset $charset\n"; | |
118 | } | |
119 | } | |
120 | else { | |
121 | die "Cannot get database collations\n"; | |
122 | } | |
123 | } | |
124 | ||
e4ec7d21 | 125 | # get a list of existing tables from the database |
c19e2794 | 126 | my $st = $db->{dbh}->prepare('show table status') |
e4ec7d21 TC |
127 | or die "Cannot prepare 'show tables': ",$db->{dbh}->errstr,"\n"; |
128 | $st->execute | |
129 | or die "Cannot execute 'show tables': ",$st->errstr,"\n"; | |
130 | ||
131 | my %ctables; | |
c19e2794 | 132 | my %current_engines; |
e4ec7d21 TC |
133 | while (my $row = $st->fetchrow_arrayref) { |
134 | $ctables{lc $row->[0]} = 1; | |
c19e2794 | 135 | $current_engines{lc $row->[0]} = $row->[1]; |
e4ec7d21 TC |
136 | } |
137 | ||
138 | # ok, we know about the tables, check the database | |
139 | for my $table (sort keys %tables) { | |
c19e2794 TC |
140 | my $want_engine = $tables{$table}{engine}; |
141 | ||
142 | print "Table $table\n" | |
143 | if $verbose; | |
144 | ||
e4ec7d21 TC |
145 | if (!$ctables{$table}) { |
146 | # table doesn't exist - build it | |
c19e2794 TC |
147 | make_table($table, $tables{$table}{cols}, $tables{$table}{indices}, |
148 | $want_engine); | |
e4ec7d21 TC |
149 | } |
150 | else { | |
151 | my $cols = $tables{$table}{cols}; | |
ac68f742 TC |
152 | my @ccols; |
153 | if ($charset) { | |
154 | @ccols = get_result("show full columns from $table"); | |
155 | } | |
156 | else { | |
157 | @ccols = get_result("describe $table"); | |
158 | } | |
e4ec7d21 TC |
159 | @ccols <= @$cols |
160 | or die "The $table table is bigger in your database"; | |
4cf6e6c5 | 161 | my @alters; |
c19e2794 TC |
162 | if ($want_engine && |
163 | lc $want_engine ne lc $current_engines{lc $table}) { | |
ac68f742 | 164 | print "Changing engine type to $want_engine\n" |
c19e2794 | 165 | if $verbose; |
139ec4b4 | 166 | push @alters, qq!engine = $want_engine!; |
c19e2794 | 167 | } |
ac68f742 TC |
168 | |
169 | # preprocess the types. | |
170 | for my $i (0 .. $#ccols) { | |
e4ec7d21 TC |
171 | my $col = $cols->[$i]; |
172 | my $ccol = $ccols[$i]; | |
ac68f742 | 173 | |
718a070d TC |
174 | if ($ccol->{type} =~ /^varchar\((\d+)\) binary$/) { |
175 | $ccol->{type} = "varbinary($1)"; | |
176 | } | |
ac68f742 TC |
177 | if ($charset) { |
178 | $ccol->{type} =~ s/char/binary/; | |
179 | $ccol->{type} =~ s/text/blob/; | |
180 | } | |
e4ec7d21 | 181 | defined $ccol->{default} or $ccol->{default} = 'NULL'; |
4cf6e6c5 TC |
182 | if ($col->{type} eq 'timestamp') { |
183 | $col->{default} = $ccol->{default} = 'current_timestamp'; | |
184 | } | |
ac68f742 TC |
185 | } |
186 | ||
187 | if ($charset) { | |
188 | # check all columns are the right charset/collation | |
189 | my $all_right = 1; | |
190 | for my $col_index (0..$#ccols) { | |
191 | my $col = $cols->[$col_index]; | |
192 | my $ccol = $ccols[$col_index]; | |
193 | if ($col->{type} =~ /char|text/i | |
194 | && (!$ccol->{collation} | |
195 | || $ccol->{collation} ne $collation)) { | |
196 | $all_right = 0; | |
197 | } | |
198 | } | |
199 | ||
200 | unless ($all_right) { | |
201 | print "Changing charset/collation to $charset/$collation\n"; | |
202 | push @alters, "convert to character set '$charset' collate '$collation'"; | |
203 | } | |
204 | push @alters, "character set '$charset' collate '$collation'"; | |
205 | } | |
206 | for my $i (0..$#ccols) { | |
207 | my $col = $cols->[$i]; | |
208 | my $ccol = $ccols[$i]; | |
209 | ||
e4ec7d21 TC |
210 | $col->{field} eq $ccol->{field} |
211 | or die "Field name mismatch old: $ccol->{field} new: $col->{field}\n"; | |
4cf6e6c5 TC |
212 | |
213 | if ($col->{null} ne $ccol->{null} | |
214 | || $col->{type} ne $ccol->{type} | |
215 | || $col->{default} ne $ccol->{default}) { | |
e4ec7d21 | 216 | print "fixing type or default for $col->{field}\n" if $verbose; |
718a070d | 217 | if ($verbose > 1) { |
4cf6e6c5 TC |
218 | print "old null: $ccol->{null} new null: $col->{null}\n" |
219 | if $ccol->{null} ne $col->{null}; | |
718a070d TC |
220 | print "old type: $ccol->{type} new type: $col->{type}\n" |
221 | if $ccol->{type} ne $col->{type}; | |
222 | print "old default: $ccol->{default} new default: $col->{default}\n" | |
223 | if $ccol->{default} ne $col->{default}; | |
224 | } | |
4cf6e6c5 | 225 | push @alters, ' modify ' . create_clauses($col); |
e4ec7d21 TC |
226 | } |
227 | } | |
228 | for my $i (@ccols .. $#$cols) { | |
229 | my $col = $cols->[$i]; | |
230 | print "Adding column $col->{field}\n" if $verbose; | |
4cf6e6c5 TC |
231 | push @alters, 'add ' . create_clauses($col); |
232 | } | |
233 | if (@alters) { | |
234 | my $sql = "alter table $table ".join(', ', @alters); | |
e4ec7d21 | 235 | run_sql($sql) |
4cf6e6c5 | 236 | or die "Cannot run $sql (column type/default/null): $DBI::errstr\n"; |
e4ec7d21 TC |
237 | } |
238 | } | |
239 | ||
240 | if (!$ctables{$table} && $pretend) { | |
241 | print "Cannot check indexes for $table since\n", | |
242 | "it doesn't exist and we're pretending.\n" if $verbose; | |
243 | next; | |
244 | } | |
245 | # indices | |
246 | # which ones exist | |
247 | my %cindices; | |
248 | for my $row (get_result("show index from $table")) { | |
249 | $cindices{$row->{key_name}} = 1; | |
250 | } | |
251 | my $indices = $tables{$table}{indices}; | |
252 | for my $name (grep $_ ne 'PRIMARY', keys %$indices) { | |
253 | next if $cindices{$name}; | |
e4ec7d21 | 254 | my $index = $indices->{$name}; |
dc872a32 | 255 | print "Creating index $name(@{$index->{cols}}) for $table\n" if $verbose; |
e4ec7d21 TC |
256 | |
257 | my $sql = "alter table $table add "; | |
258 | $sql .= $index->{unique} ? "unique " : "index "; | |
259 | $sql .= $name . " "; | |
05a89f13 | 260 | $sql .= "(" . join(",", map("`$_`", @{$index->{cols}})) . ")"; |
e4ec7d21 TC |
261 | |
262 | run_sql($sql) | |
263 | or die "Cannot add index $name: $DBI::errstr\n"; | |
264 | } | |
265 | } | |
266 | ||
267 | sub make_table { | |
c19e2794 | 268 | my ($name, $cols, $indices, $engine) = @_; |
e4ec7d21 TC |
269 | |
270 | print "Creating table $name\n" if $verbose; | |
271 | my @def = create_clauses(@$cols); | |
272 | if ($indices->{PRIMARY}) { | |
273 | push(@def, 'primary key ('.join(',', @{$indices->{PRIMARY}{cols}}).')'); | |
274 | } | |
275 | my $sql = "create table $name (\n"; | |
276 | $sql .= join(",\n", @def); | |
c19e2794 | 277 | $sql .= "\n)"; |
ac68f742 | 278 | my @extras; |
c19e2794 | 279 | if (defined $engine) { |
139ec4b4 | 280 | push @extras, "engine = $engine"; |
ac68f742 TC |
281 | } |
282 | if ($charset) { | |
283 | push @extras, | |
284 | "character set '$charset'", | |
285 | "collate '$collation'"; | |
286 | } | |
287 | if (@extras) { | |
288 | $sql .= join (", ", @extras); | |
c19e2794 TC |
289 | } |
290 | $sql .= "\n"; | |
e4ec7d21 TC |
291 | print "SQL to create $name: $sql\n" if $verbose > 2; |
292 | run_sql($sql) | |
293 | or die "Cannot create table $name\n"; | |
294 | } | |
295 | ||
296 | sub run_sql { | |
297 | my ($sql, @args) = @_; | |
298 | ||
299 | print "run_sql($sql, @args)\n" if $verbose > 1; | |
300 | return 1 if $pretend; | |
301 | my $sth = $db->{dbh}->prepare($sql) | |
302 | or die "Cannot prepare $sql: ",$db->{dbh}->errstr; | |
303 | return $sth->execute(@args); | |
304 | } | |
305 | ||
306 | sub get_result { | |
307 | my ($sql, @args) = @_; | |
308 | ||
309 | print "get_result($sql, @args)\n" if $verbose > 1; | |
310 | my $sth = $db->{dbh}->prepare($sql) | |
311 | or die "Cannot prepare $sql: ",$db->{dbh}->errstr; | |
312 | $sth->execute(@args) | |
313 | or die "Cannot execute $sql (@args): ",$sth->errstr; | |
314 | my @results; | |
315 | while (my $row = $sth->fetchrow_hashref('NAME_lc')) { | |
316 | push(@results, { %$row }); | |
317 | } | |
318 | ||
319 | @results; | |
320 | } | |
321 | ||
322 | sub create_clauses { | |
323 | my (@cols) = @_; | |
324 | ||
325 | my @results; | |
326 | for my $col (@cols) { | |
05a89f13 | 327 | my $sql = "`" . $col->{field} . "` " . $col->{type}; |
ac68f742 TC |
328 | if ($charset && $col->{type} =~ /char|text/) { |
329 | $sql .= " character set '$charset' collate '$collation'"; | |
330 | } | |
6d541bf9 | 331 | $sql .= $col->{null} eq 'YES' ? ' null' : ' not null'; |
74b21f6d TC |
332 | if ($col->{default} ne 'NULL' && |
333 | ($col->{type} =~ /char/i || $col->{default} =~ /\d/)) { | |
e4ec7d21 TC |
334 | $sql .= " default "; |
335 | if ($col->{default} =~ /^\d+$/) { | |
336 | $sql .= $col->{default}; | |
337 | } | |
338 | else { | |
339 | $sql .= $db->{dbh}->quote($col->{default}); | |
340 | } | |
341 | } | |
342 | if ($col->{extra}) { | |
343 | $sql .= " ".$col->{extra}; | |
344 | } | |
345 | push(@results, $sql); | |
346 | } | |
347 | ||
348 | if (wantarray) { | |
349 | return @results; | |
350 | } | |
351 | else { | |
352 | @results == 1 or die "Programming error!"; | |
353 | return $results[0]; | |
354 | } | |
355 | } | |
6e3d2da5 | 356 | |
dc872a32 TC |
357 | sub help { |
358 | # dump the POD up to the AUTHOR heading | |
359 | while (<DATA>) { | |
360 | last if /^=head1 AUTHOR/; | |
361 | print; | |
362 | } | |
363 | exit; | |
364 | } | |
365 | ||
366 | __DATA__ | |
6e3d2da5 TC |
367 | |
368 | =head1 NAME | |
369 | ||
370 | upgrade_mysql.pl - upgrades the sites mysql database to the description in mysql.str | |
371 | ||
372 | =head1 SYNOPSIS | |
373 | ||
374 | perl upgrade_mysql.pl [-bn] [-v [verbosity]] | |
375 | ||
376 | =head1 DESCRIPTION | |
377 | ||
378 | Upgrades your BSE database, as described in your Constants.pm to the | |
379 | schema described in mysql.str. | |
380 | ||
381 | BACKUP YOUR DATABASE BEFORE USING THIS TOOL. | |
382 | ||
383 | =head1 OPTIONS | |
384 | ||
385 | =over | |
386 | ||
387 | =item -b | |
388 | ||
389 | Asserts that the user has done a backup. Avoids the interactive query | |
390 | about having done a backup. | |
391 | ||
392 | =item -n | |
393 | ||
394 | Only check for the changes needed, rather than actually performing the | |
395 | upgrade. Since it's possible that tables might not exist when | |
396 | checking for indices, this may give you some errors. | |
397 | ||
398 | =item -v [verbosity] | |
399 | ||
400 | Controls verbosity of output. The default level (1), will produce | |
401 | basic descriptions of what is happening, including which table is | |
402 | being checked, and any changes being made. | |
403 | ||
404 | Level 2 will print debug messages containing any SQL that's being | |
405 | executed. | |
406 | ||
407 | Level 3 prints information useful only to developers. | |
408 | ||
dc872a32 TC |
409 | =item -i filename |
410 | ||
411 | Specify and input filename that isn't mysql.str. | |
412 | ||
413 | =item -h | |
414 | ||
415 | Display help. | |
416 | ||
6e3d2da5 TC |
417 | =back |
418 | ||
419 | =head1 AUTHOR | |
420 | ||
421 | Tony Cook <tony@develop-help.com> | |
422 | ||
423 | =head1 REVISION | |
424 | ||
425 | $Revision$ | |
426 | ||
427 | =head1 SEE ALSO | |
428 | ||
429 | bse | |
430 | ||
431 | =cut |