]> git.imager.perl.org - bse.git/blame - site/util/upgrade_mysql.pl
make the searchindex table myisam, it has no use for ACID
[bse.git] / site / util / upgrade_mysql.pl
CommitLineData
e4ec7d21
TC
1#!perl -w
2use strict;
3use lib '../cgi-bin/modules';
4use DBI;
5use BSE::DB;
6use Getopt::Long;
d2473dc2
TC
7use BSE::API qw(bse_init);
8use Cwd;
9
10bse_init("../cgi-bin");
e4ec7d21
TC
11
12my $verbose;
13my $pretend;
14my $didbackup;
dc872a32
TC
15my $input = "mysql.str";
16my $wanthelp;
ac68f742
TC
17my $charset;
18my $collation;
e4ec7d21
TC
19
20Getopt::Long::Configure('bundling');
ac68f742
TC
21GetOptions
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
34help() if $wanthelp;
35
e4ec7d21
TC
36if ($didbackup) {
37 print "Since you gave the -b option, I assume you made a backup.\n";
38}
39else {
40 my $conf = int(rand(1000))+1;
41 print <<EOS;
42This tool attempts to add missing tables, columns and indices to your
43database.
44
45It's possible it will mess up.
46
47If you haven't made a backup of your database $conf, MAKE ONE NOW.
48
49If you have made a backup of your database enter the number in the
50middle of the previous paragraph. Any other entry will abort.
51EOS
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
60my $db = BSE::DB->single;
61
62UNIVERSAL::isa($db, 'BSE::DB::Mysql')
d2473dc2 63 or die "Sorry, this only works for Mysql databases ($db)\n";
e4ec7d21 64
dc872a32
TC
65open STRUCT, "< $input"
66 or die "Cannot open structure file $input: $!\n";
e4ec7d21
TC
67my %tables;
68my $table;
69while (<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}
102close STRUCT;
103
ac68f742
TC
104my %coll_map;
105if ($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 126my $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
131my %ctables;
c19e2794 132my %current_engines;
e4ec7d21
TC
133while (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
139for 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
267sub 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
296sub 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
306sub 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
322sub 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
357sub 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
370upgrade_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
378Upgrades your BSE database, as described in your Constants.pm to the
379schema described in mysql.str.
380
381BACKUP YOUR DATABASE BEFORE USING THIS TOOL.
382
383=head1 OPTIONS
384
385=over
386
387=item -b
388
389Asserts that the user has done a backup. Avoids the interactive query
390about having done a backup.
391
392=item -n
393
394Only check for the changes needed, rather than actually performing the
395upgrade. Since it's possible that tables might not exist when
396checking for indices, this may give you some errors.
397
398=item -v [verbosity]
399
400Controls verbosity of output. The default level (1), will produce
401basic descriptions of what is happening, including which table is
402being checked, and any changes being made.
403
404Level 2 will print debug messages containing any SQL that's being
405executed.
406
407Level 3 prints information useful only to developers.
408
dc872a32
TC
409=item -i filename
410
411Specify and input filename that isn't mysql.str.
412
413=item -h
414
415Display help.
416
6e3d2da5
TC
417=back
418
419=head1 AUTHOR
420
421Tony Cook <tony@develop-help.com>
422
423=head1 REVISION
424
425$Revision$
426
427=head1 SEE ALSO
428
429bse
430
431=cut