8c87ab29904e027677b69df66640fbf5c87db0e9
[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   push @bugfix, "SELECT SETVAL( 'upgrade_journal_upgradenum_seq',
98                                 ( SELECT MAX(upgradenum) FROM upgrade_journal )
99                               )
100                 ";
101 }
102
103 if ( $DRY_RUN ) {
104   print
105     join(";\n", @bugfix ). ";\n";
106 } elsif ( @bugfix ) {
107
108   foreach my $statement ( @bugfix ) {
109     warn "$statement\n";
110     $dbh->do( $statement )
111       or die "Error: ". $dbh->errstr. "\n executing: $statement";
112   }
113
114   upgrade_schema(%upgrade_opts);
115
116   dbdef_create($dbh, $dbdef_file);
117   delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
118   reload_dbdef($dbdef_file);
119
120 }
121
122 #you should have run fs-migrate-part_svc ages ago, when you upgraded
123 #from 1.3 to 1.4... if not, it needs to be hooked into -upgrade here or
124 #you'll lose all the part_svc settings it migrates to part_svc_column
125
126 my $conf = new FS::Conf;
127
128 my $dbdef_dist = dbdef_dist(
129   datasrc,
130   { 'queue-no_history' => $conf->exists('queue-no_history') },
131 );
132
133 my @statements = dbdef->sql_update_schema( $dbdef_dist,
134                                            $dbh,
135                                            { 'nullify_default' => 1, },
136                                          );
137
138 #### NEW CUSTOM FIELDS:
139 # 1. prevent new custom field columns from being dropped by upgrade
140 # 2. migrate old virtual fields to real fields (new custom fields)
141 ####
142 my $cfsth = $dbh->prepare("SELECT * FROM part_virtual_field") 
143                                                          or die $dbh->errstr;
144 $cfsth->execute or die $cfsth->errstr;
145 my $cf; 
146 while ( $cf = $cfsth->fetchrow_hashref ) {
147     my $tbl = $cf->{'dbtable'};
148     my $name = $cf->{'name'};
149     $name = lc($name) unless driver_name =~ /^mysql/i;
150
151     @statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
152                                                                     @statements;
153     push @statements, 
154         "ALTER TABLE $tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
155      unless (dbdef->table($tbl) && dbdef->table($tbl)->column("cf_$name"));
156     push @statements, 
157         "ALTER TABLE h_$tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
158      unless (dbdef->table("h_$tbl") && dbdef->table("h_$tbl")->column("cf_$name"));
159 }
160 warn "Custom fields schema upgrade completed";
161
162 @statements = 
163   grep { $_ !~ /^CREATE +INDEX +h_queue/i } #useless, holds up queue insertion
164        @statements;
165
166 unless ( driver_name =~ /^mysql/i ) {
167   #not necessary under non-mysql, takes forever on big db
168   @statements =
169     grep { $_ !~ /^ *ALTER +TABLE +h_queue +ALTER +COLUMN +job +TYPE +varchar\(512\) *$/i }
170          @statements;
171 }
172
173 if ( $opt_c ) {
174
175   @statements =
176     grep { $_ !~ /^ *ALTER +TABLE +(h_)?cdr /i }
177          @statements;
178
179   @statements =
180     grep { $_ !~ /^ *CREATE +INDEX +(h_)?cdr\d+ /i }
181          @statements;
182
183 }
184
185 if ( $DRY_RUN ) {
186   print
187     join(";\n", @statements ). ";\n";
188   exit;
189 } else {
190
191   my @clones = ();
192   foreach my $statement ( @statements ) {
193
194     if ( $opt_a ) {
195
196       my $clone = '';
197       until ( $clone = $dbh->clone ) {
198         sleep 60; #too many database connections?  wait and retry
199       }
200       until ( $clone->do( $statement, {pg_async=>PG_ASYNC} ) ) {
201         sleep 60; #too many ... running queries?  wait and retry
202       }
203       warn "$statement\n";
204       push @clones, $clone;
205
206     } else {
207       warn "$statement\n";
208       $dbh->do( $statement )
209         or die "Error: ". $dbh->errstr. "\n executing: $statement";
210     }
211
212   }
213
214   warn "Waiting for all schema changes to complete\n" if @clones; # && $DEBUG;
215   while ( @clones ) {
216     my @newclones = ();
217     foreach my $clone ( @clones ) {
218       if ( $clone->pg_ready ) {
219         $clone->pg_result or die $clone->errstr;
220         $clone->commit    or die $clone->errstr;
221       } else {
222         push @newclones, $clone;
223       }
224     }
225     @clones = @newclones;
226     sleep 30 if @clones;
227   }
228
229 #  warn "Pre-schema change upgrades completed in ". (time-$start). " seconds\n"; # if $DEBUG;
230 #  $start = time;
231
232 #  dbdef->update_schema( dbdef_dist(datasrc), $dbh );
233 }
234
235 warn "Schema upgrade completed in ". (time-$start). " seconds\n"; # if $DEBUG;
236 $start = time;
237
238 my $hashref = {};
239 $hashref->{dry_run} = 1 if $DRY_RUN;
240 $hashref->{debug} = 1 if $DEBUG && $DRY_RUN;
241 prune_applications($hashref) unless $opt_s;
242
243 warn "Application pruning completed in ". (time-$start). " seconds\n"; # if $DEBUG;
244 $start = time;
245
246 print "\n" if $DRY_RUN;
247
248 if ( $dbh->{Driver}->{Name} =~ /^mysql/i && ! $opt_s ) {
249
250   foreach my $table (qw( svc_acct svc_phone )) {
251
252     my $sth = $dbh->prepare(
253       "SELECT COUNT(*) FROM duplicate_lock WHERE lockname = '$table'"
254     ) or die $dbh->errstr;
255
256     $sth->execute or die $sth->errstr;
257
258     unless ( $sth->fetchrow_arrayref->[0] ) {
259
260       $sth = $dbh->prepare(
261         "INSERT INTO duplicate_lock ( lockname ) VALUES ( '$table' )"
262       ) or die $dbh->errstr;
263
264       $sth->execute or die $sth->errstr;
265
266     }
267
268   }
269
270   warn "Duplication lock creation completed in ". (time-$start). " seconds\n"; # if $DEBUG;
271   $start = time;
272
273 }
274
275 $dbh->commit or die $dbh->errstr;
276
277 dbdef_create($dbh, $dbdef_file);
278
279 $dbh->disconnect or die $dbh->errstr;
280
281 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
282 $FS::UID::AutoCommit = 0;
283 $FS::UID::callback_hack = 1;
284 $dbh = adminsuidsetup($user);
285 $FS::UID::callback_hack = 0;
286 unless ( $DRY_RUN || $opt_s ) {
287   my $dir = "%%%FREESIDE_CONF%%%/conf.". datasrc;
288   if (!scalar(qsearch('conf', {}))) {
289     my $error = FS::Conf::init_config($dir);
290     if ($error) {
291       warn "CONFIGURATION UPGRADE FAILED\n";
292       $dbh->rollback or die $dbh->errstr;
293       die $error;
294     }
295   }
296 }
297 $dbh->commit or die $dbh->errstr;
298 $dbh->disconnect or die $dbh->errstr;
299
300 $FS::UID::AutoCommit = 1;
301
302 $dbh = adminsuidsetup($user);
303
304 warn "Re-initialization with updated schema completed in ". (time-$start). " seconds\n"; # if $DEBUG;
305 $start = time;
306
307 #### NEW CUSTOM FIELDS:
308 # 3. migrate old virtual field data to the new custom fields
309 ####
310 $cfsth = $dbh->prepare("SELECT * FROM virtual_field left join part_virtual_field using (vfieldpart)")
311                                                          or die $dbh->errstr;
312 $cfsth->execute or die $cfsth->errstr;
313 my @cfst;
314 while ( $cf = $cfsth->fetchrow_hashref ) {
315     my $tbl = $cf->{'dbtable'};
316     my $name = $cf->{'name'};
317     my $dtable = dbdef->table($tbl);
318     next unless $dtable && $dtable->primary_key; # XXX: warn first?
319     my $pkey = $dtable->primary_key;
320     next unless $dtable->column($pkey)->type =~ /int/i; # XXX: warn first?
321     push @cfst, "UPDATE $tbl set cf_$name = '".$cf->{'value'}."' WHERE $pkey = ".$cf->{'recnum'};
322     push @cfst, "DELETE FROM virtual_field WHERE vfieldnum = ".$cf->{'vfieldnum'};
323 }
324 foreach my $cfst ( @cfst ) {
325     warn "$cfst\n";
326     $dbh->do( $cfst )
327       or die "Error: ". $dbh->errstr. "\n executing: $cfst";
328 }
329 warn "Custom fields data upgrade completed";
330
331 upgrade_config(%upgrade_opts)
332   unless $DRY_RUN || $opt_s;
333
334 $dbh->commit or die $dbh->errstr;
335
336 warn "Config updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
337 $start = time;
338
339 upgrade(%upgrade_opts)
340   unless $DRY_RUN || $opt_s;
341
342 $dbh->commit or die $dbh->errstr;
343
344 warn "Table updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
345 $start = time;
346
347 upgrade_sqlradius(%upgrade_opts)
348   unless $DRY_RUN || $opt_s || $opt_r;
349
350 warn "SQL RADIUS updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
351 $start = time;
352
353 $dbh->commit or die $dbh->errstr;
354 $dbh->disconnect or die $dbh->errstr;
355
356 warn "Final commit and disconnection completed in ". (time-$start). " seconds; upgrade done!\n"; # if $DEBUG;
357
358 ###
359
360 sub dbdef_create { # reverse engineer the schema from the DB and save to file
361   my( $dbh, $file ) = @_;
362   my $dbdef = new_native DBIx::DBSchema $dbh;
363   $dbdef->save($file);
364 }
365
366 sub usage {
367   die "Usage:\n  freeside-upgrade [ -d ] [ -q | -v ] [ -r ] [ -c ] [ -s ] [ -j ] [ -a ] user\n"; 
368 }
369
370 =head1 NAME
371
372 freeside-upgrade - Upgrades database schema for new freeside verisons.
373
374 =head1 SYNOPSIS
375
376   freeside-upgrade [ -d ] [ -q | -v ] [ -r ] [ -c ] [ -s ] [ -j ] [ -a ]
377
378 =head1 DESCRIPTION
379
380 Reads your existing database schema and updates it to match the current schema,
381 adding any columns or tables necessary.
382
383 Also performs other upgrade functions:
384
385 =over 4
386
387 =item Calls FS:: Misc::prune::prune_applications (probably unnecessary every upgrade, but simply won't find any records to change)
388
389 =item If necessary, moves your configuration information from the filesystem in /usr/local/etc/freeside/conf.<datasrc> to the database.
390
391 =back
392
393   [ -d ]: Dry run; output SQL statements (to STDOUT) only, but do not execute
394           them.
395
396   [ -q ]: Run quietly.  This may become the default at some point.
397
398   [ -v ]: Run verbosely, sending debugging information to STDERR.  This is the
399           current default.
400
401   [ -s ]: Schema changes only.  Useful for Pg/slony slaves where the data
402
403   [ -r ]: Skip sqlradius updates.  Useful for occassions where the sqlradius
404           databases may be inaccessible.
405
406   [ -c ]: Skip cdr and h_cdr updates.
407
408           changes will be replicated from the Pg/slony master.
409
410   [ -j ]: Run certain upgrades asychronously from the job queue.  Currently 
411           used only for the 2.x -> 3.x cust_location, cust_pay and part_pkg
412           upgrades.  This may cause odd behavior before the upgrade is
413           complete, so it's recommended only for very large cust_main, cust_pay
414           and/or part_pkg tables that take too long to upgrade.
415
416   [ -a ]: Run schema changes in parallel (Pg only).  DBIx::DBSchema minimum 
417           version 0.41 recommended.  Recommended only for large databases and
418           powerful database servers, to reduce upgrade time.
419
420 =head1 SEE ALSO
421
422 =cut
423