stray closing /TABLE in the no-ticket case
[freeside.git] / FS / bin / freeside-upgrade
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw( $opt_d $opt_s $opt_q $opt_v $opt_r $opt_c $opt_j $opt_a );
5 use vars qw( $DEBUG $DRY_RUN );
6 use Getopt::Std;
7 use DBD::Pg qw(:async); #for -a
8 use DBIx::DBSchema 0.46;
9 use FS::UID qw(adminsuidsetup checkeuid datasrc driver_name);
10 use FS::CurrentUser;
11 use FS::Schema qw( dbdef dbdef_dist reload_dbdef );
12 use FS::Misc::prune qw(prune_applications);
13 use FS::Conf;
14 use FS::Record qw(qsearch);
15 use FS::Upgrade qw(upgrade_schema upgrade_config upgrade upgrade_sqlradius);
16
17 my $start = time;
18
19 die "Not running uid freeside!" unless checkeuid();
20
21 getopts("dqrcsja");
22
23 $DEBUG = !$opt_q;
24 #$DEBUG = $opt_v;
25
26 $DRY_RUN = $opt_d;
27
28 my $user = shift or die &usage;
29 $FS::CurrentUser::upgrade_hack = 1;
30 $FS::UID::callback_hack = 1;
31 my $dbh = adminsuidsetup($user);
32 $FS::UID::callback_hack = 0;
33
34 # pass command line opts through to upgrade* routines
35 my %upgrade_opts = (
36   quiet   => $opt_q,
37   verbose => $opt_v,
38   queue   => $opt_j,
39   # others?
40 );
41
42 if ( driver_name =~ /^mysql/i ) { #until 0.39 is required above
43   eval "use DBIx::DBSchema 0.39;";
44   die $@ if $@;
45 }
46
47 #needs to match FS::Schema...
48 my $dbdef_file = "%%%FREESIDE_CONF%%%/dbdef.". datasrc;
49
50 dbdef_create($dbh, $dbdef_file);
51
52 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
53 reload_dbdef($dbdef_file);
54
55 warn "Upgrade startup completed in ". (time-$start). " seconds\n"; # if $DEBUG;
56 $start = time;
57
58 #$DBIx::DBSchema::DEBUG = $DEBUG;
59 #$DBIx::DBSchema::Table::DEBUG = $DEBUG;
60 #$DBIx::DBSchema::Index::DEBUG = $DEBUG;
61
62 my @bugfix = ();
63
64 if ( $DRY_RUN ) {
65   print join(";\n", @bugfix ). ";\n";
66 } else {
67   foreach my $statement ( @bugfix ) {
68     warn "$statement\n";
69     $dbh->do( $statement )
70       or die "Error: ". $dbh->errstr. "\n executing: $statement";
71   }
72 }
73
74 ###
75 # Fixes before schema upgrade
76 ###
77 # this isn't actually the main schema upgrade, this calls _upgrade_schema
78 # in any class that has it
79 if ( $DRY_RUN ) {
80   #XXX no dry run for upgrade_schema stuff yet.
81   # looking at the code some are a mix of SQL statements and our methods, icky.
82   # its not like dry run is 100% anyway, all sort of other later upgrade tasks
83   # aren't printed either
84 } else {
85   upgrade_schema(%upgrade_opts);
86
87   dbdef_create($dbh, $dbdef_file);
88   delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
89   reload_dbdef($dbdef_file);
90 }
91
92 ###
93 # Now here is the main/automatic schema upgrade via DBIx::DBSchema
94 ###
95
96 my $conf = new FS::Conf;
97
98 my $dbdef_dist = dbdef_dist(
99   datasrc,
100   { 'queue-no_history' => $conf->exists('queue-no_history') },
101 );
102
103 my @statements = dbdef->sql_update_schema( $dbdef_dist,
104                                            $dbh,
105                                            { 'nullify_default' => 1, },
106                                          );
107
108 ###
109 # New custom fields
110 ###
111 # 1. prevent new custom field columns from being dropped by upgrade
112 # 2. migrate old virtual fields to real fields (new custom fields)
113
114 my $cfsth = $dbh->prepare("SELECT * FROM part_virtual_field") 
115                                                          or die $dbh->errstr;
116 $cfsth->execute or die $cfsth->errstr;
117 my $cf; 
118 while ( $cf = $cfsth->fetchrow_hashref ) {
119     my $tbl = $cf->{'dbtable'};
120     my $name = $cf->{'name'};
121     $name = lc($name) unless driver_name =~ /^mysql/i;
122
123     @statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
124                                                                     @statements;
125     push @statements, 
126         "ALTER TABLE $tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
127      unless (dbdef->table($tbl) && dbdef->table($tbl)->column("cf_$name"));
128     push @statements, 
129         "ALTER TABLE h_$tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
130      unless (dbdef->table("h_$tbl") && dbdef->table("h_$tbl")->column("cf_$name"));
131 }
132 warn "Custom fields schema upgrade completed";
133
134 ###
135 # Other stuff
136 ###
137
138 @statements = 
139   grep { $_ !~ /^CREATE +INDEX +h_queue/i } #useless, holds up queue insertion
140        @statements;
141
142 unless ( driver_name =~ /^mysql/i ) {
143   #not necessary under non-mysql, takes forever on big db
144   @statements =
145     grep { $_ !~ /^ *ALTER +TABLE +h_queue +ALTER +COLUMN +job +TYPE +varchar\(512\) *$/i }
146          @statements;
147 }
148
149 if ( $opt_c ) {
150
151   #can always add it back for 4.x->4.x if we need it
152   die "FATAL: -c removed: cdr / h_cdr upgrade is required for 4.x\n";
153
154   @statements =
155     grep { $_ !~ /^ *ALTER +TABLE +(h_)?cdr /i }
156          @statements;
157
158   @statements =
159     grep { $_ !~ /^ *CREATE +INDEX +(h_)?cdr\d+ /i }
160          @statements;
161
162 }
163
164
165 ###
166 # Now run the @statements
167 ###
168
169 if ( $DRY_RUN ) {
170   print
171     join(";\n", @statements ). ";\n";
172   exit;
173 } elsif ( $opt_a ) {
174
175   ###
176   # -a: Run schema changes in parallel (Pg only).
177   ###
178
179   my $MAX_HANDLES; # undef for now, set it if you want a limit
180
181   my @phases = map { [] } 0..4;
182   my $fsupgrade_idx = 1;
183   my %idx_map;
184   foreach (@statements) {
185     if ( /^ *(CREATE|ALTER) +TABLE/ ) {
186       # phase 0: CREATE TABLE, ALTER TABLE
187       push @{ $phases[0] }, $_;
188     } elsif ( /^ *ALTER +INDEX.* RENAME TO dbs_temp(\d+)/ ) {
189       # phase 1: rename index to dbs_temp%d
190       # (see DBIx::DBSchema::Table)
191       # but in this case, uniqueify all the dbs_temps.  This method only works
192       # because they are in the right order to begin with...
193       my $dbstemp_idx = $1;
194       s/dbs_temp$dbstemp_idx/fsupgrade_temp$fsupgrade_idx/;
195       $idx_map{ $dbstemp_idx } = $fsupgrade_idx;
196       push @{ $phases[1] }, $_;
197       $fsupgrade_idx++;
198     } elsif ( /^ *(CREATE|DROP)( +UNIQUE)? +INDEX/ ) {
199       # phase 2: create/drop indices
200       push @{ $phases[2] }, $_;
201     } elsif ( /^ *ALTER +INDEX +dbs_temp(\d+) +RENAME/ ) {
202       # phase 3: rename temp indices back to real ones
203       my $dbstemp_idx = $1;
204       my $mapped_idx = $idx_map{ $dbstemp_idx }
205         or die "unable to remap dbs_temp$1 RENAME statement";
206       s/dbs_temp$dbstemp_idx/fsupgrade_temp$mapped_idx/;
207       push @{ $phases[3] }, $_;
208     } else {
209       # phase 4: everything else (CREATE SEQUENCE, SELECT SETVAL, etc.)
210       push @{ $phases[4] }, $_;
211     }
212   }
213   my $i = 0;
214   my @busy = ();
215   my @free = ();
216   foreach my $phase (@phases) {
217     warn "Starting schema changes, phase $i...\n";
218     while (@$phase or @busy) {
219       # check status of all running tasks
220       my @newbusy;
221       my $failed_clone;
222       for my $clone (@busy) {
223         if ( $clone->pg_ready ) {
224           # then clean it up
225           my $rv = $clone->pg_result && $clone->commit;
226           $failed_clone = $clone if !$rv;
227           push @free, $clone;
228         } else {
229           push @newbusy, $clone;
230         }
231       }
232       if ( $failed_clone ) {
233         my $errstr = $failed_clone->errstr;
234         foreach my $clone (@newbusy, $failed_clone) {
235           $clone->pg_cancel if $clone->{pg_async_status} == 1;
236           $clone->disconnect;
237         }
238         die "$errstr\n";
239       }
240       @busy = @newbusy;
241       if (my $statement = $phase->[0]) {
242         my $clone;
243         if ( @free ) {
244           $clone = shift(@free);
245         } elsif ( !$MAX_HANDLES or 
246                   scalar(@free) + scalar(@busy) < $MAX_HANDLES ) {
247           $clone = $dbh->clone; # this will fail if over the server limit
248         }
249
250         if ( $clone ) {
251           my $rv = $clone->do($statement, {pg_async => PG_ASYNC});
252           if ( $rv ) {
253             warn "$statement\n";
254             shift @{ $phase }; # and actually take the statement off the queue
255             push @busy, $clone;
256           } # else I don't know, wait and retry
257         } # else too many handles, wait and retry
258       } elsif (@busy) {
259         # all statements are dispatched
260         warn "Waiting for phase $i to complete\n";
261         sleep 30;
262       }
263     } # while @$phase or @busy
264     $i++;
265   } # foreach $phase
266   warn "Schema changes complete.\n";
267
268 #  warn "Pre-schema change upgrades completed in ". (time-$start). " seconds\n"; # if $DEBUG;
269 #  $start = time;
270
271 #  dbdef->update_schema( dbdef_dist(datasrc), $dbh );
272
273 } else {
274
275   ###
276   # normal case, run statements sequentially
277   ###
278
279   foreach my $statement ( @statements ) {
280     warn "$statement\n";
281     $dbh->do( $statement )
282       or die "Error: ". $dbh->errstr. "\n executing: $statement";
283   }
284 }
285
286 warn "Schema upgrade completed in ". (time-$start). " seconds\n"; # if $DEBUG;
287 $start = time;
288
289 my $hashref = {};
290 $hashref->{dry_run} = 1 if $DRY_RUN;
291 $hashref->{debug} = 1 if $DEBUG && $DRY_RUN;
292 prune_applications($hashref) unless $opt_s;
293
294 warn "Application pruning completed in ". (time-$start). " seconds\n"; # if $DEBUG;
295 $start = time;
296
297 print "\n" if $DRY_RUN;
298
299 if ( $dbh->{Driver}->{Name} =~ /^mysql/i && ! $opt_s ) {
300
301   foreach my $table (qw( svc_acct svc_phone )) {
302
303     my $sth = $dbh->prepare(
304       "SELECT COUNT(*) FROM duplicate_lock WHERE lockname = '$table'"
305     ) or die $dbh->errstr;
306
307     $sth->execute or die $sth->errstr;
308
309     unless ( $sth->fetchrow_arrayref->[0] ) {
310
311       $sth = $dbh->prepare(
312         "INSERT INTO duplicate_lock ( lockname ) VALUES ( '$table' )"
313       ) or die $dbh->errstr;
314
315       $sth->execute or die $sth->errstr;
316
317     }
318
319   }
320
321   warn "Duplication lock creation completed in ". (time-$start). " seconds\n"; # if $DEBUG;
322   $start = time;
323
324 }
325
326 $dbh->commit or die $dbh->errstr;
327
328 dbdef_create($dbh, $dbdef_file);
329
330 $dbh->disconnect or die $dbh->errstr;
331
332 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
333 $FS::UID::AutoCommit = 0;
334 $FS::UID::callback_hack = 1;
335 $dbh = adminsuidsetup($user);
336 $FS::UID::callback_hack = 0;
337 unless ( $DRY_RUN || $opt_s ) {
338   my $dir = "%%%FREESIDE_CONF%%%/conf.". datasrc;
339   if (!scalar(qsearch('conf', {}))) {
340     my $error = FS::Conf::init_config($dir);
341     if ($error) {
342       warn "CONFIGURATION UPGRADE FAILED\n";
343       $dbh->rollback or die $dbh->errstr;
344       die $error;
345     }
346   }
347 }
348 $dbh->commit or die $dbh->errstr;
349 $dbh->disconnect or die $dbh->errstr;
350
351 $FS::UID::AutoCommit = 1;
352
353 $dbh = adminsuidsetup($user);
354
355 warn "Re-initialization with updated schema completed in ". (time-$start). " seconds\n"; # if $DEBUG;
356 $start = time;
357
358 #### NEW CUSTOM FIELDS:
359 # 3. migrate old virtual field data to the new custom fields
360 ####
361 $cfsth = $dbh->prepare("SELECT * FROM virtual_field left join part_virtual_field using (vfieldpart)")
362                                                          or die $dbh->errstr;
363 $cfsth->execute or die $cfsth->errstr;
364 my @cfst;
365 while ( $cf = $cfsth->fetchrow_hashref ) {
366     my $tbl = $cf->{'dbtable'};
367     my $name = $cf->{'name'};
368     my $dtable = dbdef->table($tbl);
369     next unless $dtable && $dtable->primary_key; # XXX: warn first?
370     my $pkey = $dtable->primary_key;
371     next unless $dtable->column($pkey)->type =~ /int/i; # XXX: warn first?
372     push @cfst, "UPDATE $tbl set cf_$name = '".$cf->{'value'}."' WHERE $pkey = ".$cf->{'recnum'};
373     push @cfst, "DELETE FROM virtual_field WHERE vfieldnum = ".$cf->{'vfieldnum'};
374 }
375 foreach my $cfst ( @cfst ) {
376     warn "$cfst\n";
377     $dbh->do( $cfst )
378       or die "Error: ". $dbh->errstr. "\n executing: $cfst";
379 }
380 warn "Custom fields data upgrade completed";
381
382 upgrade_config(%upgrade_opts)
383   unless $DRY_RUN || $opt_s;
384
385 $dbh->commit or die $dbh->errstr;
386
387 warn "Config updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
388 $start = time;
389
390 upgrade(%upgrade_opts)
391   unless $DRY_RUN || $opt_s;
392
393 $dbh->commit or die $dbh->errstr;
394
395 warn "Table updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
396 $start = time;
397
398 upgrade_sqlradius(%upgrade_opts)
399   unless $DRY_RUN || $opt_s || $opt_r;
400
401 warn "SQL RADIUS updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
402 $start = time;
403
404 $dbh->commit or die $dbh->errstr;
405 $dbh->disconnect or die $dbh->errstr;
406
407 warn "Final commit and disconnection completed in ". (time-$start). " seconds; upgrade done!\n"; # if $DEBUG;
408
409 ###
410
411 sub dbdef_create { # reverse engineer the schema from the DB and save to file
412   my( $dbh, $file ) = @_;
413   my $dbdef = new_native DBIx::DBSchema $dbh;
414   $dbdef->save($file);
415 }
416
417 sub usage {
418   die "Usage:\n  freeside-upgrade [ -d ] [ -q | -v ] [ -r ] [ -s ] [ -j ] [ -a ] user\n"; 
419 }
420
421 =head1 NAME
422
423 freeside-upgrade - Upgrades database schema for new freeside verisons.
424
425 =head1 SYNOPSIS
426
427   freeside-upgrade [ -d ] [ -q | -v ] [ -r ] [ -s ] [ -j ] [ -a ]
428
429 =head1 DESCRIPTION
430
431 Reads your existing database schema and updates it to match the current schema,
432 adding any columns or tables necessary.
433
434 Also performs other upgrade functions:
435
436 =over 4
437
438 =item Calls FS:: Misc::prune::prune_applications (probably unnecessary every upgrade, but simply won't find any records to change)
439
440 =item If necessary, moves your configuration information from the filesystem in /usr/local/etc/freeside/conf.<datasrc> to the database.
441
442 =back
443
444   [ -d ]: Dry run; output SQL statements (to STDOUT) only, but do not execute
445           them.
446
447   [ -q ]: Run quietly.  This may become the default at some point.
448
449   [ -v ]: Run verbosely, sending debugging information to STDERR.  This is the
450           current default.
451
452   [ -s ]: Schema changes only.  Useful for Pg/slony slaves where the data
453           changes will be replicated from the Pg/slony master.
454
455   [ -r ]: Skip sqlradius updates.  Useful for occassions where the sqlradius
456           databases may be inaccessible.
457
458   [ -j ]: Run certain upgrades asychronously from the job queue.  Currently 
459           used only for the 2.x -> 3.x cust_location, cust_pay and part_pkg
460           upgrades.  This may cause odd behavior before the upgrade is
461           complete, so it's recommended only for very large cust_main, cust_pay
462           and/or part_pkg tables that take too long to upgrade.
463
464   [ -a ]: Run schema changes in parallel (Pg only).  DBIx::DBSchema minimum 
465           version 0.41 recommended.  Recommended only for large databases and
466           powerful database servers, to reduce upgrade time.
467
468 =head1 SEE ALSO
469
470 =cut
471