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