custom fields, RT11714
[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);
5 use vars qw($DEBUG $DRY_RUN);
6 use Getopt::Std;
7 use DBIx::DBSchema 0.31; #0.39
8 use FS::UID qw(adminsuidsetup checkeuid datasrc driver_name);  #getsecrets);
9 use FS::CurrentUser;
10 use FS::Schema qw( dbdef dbdef_dist reload_dbdef );
11 use FS::Misc::prune qw(prune_applications);
12 use FS::Conf;
13 use FS::Record qw(qsearch);
14 use FS::Upgrade qw(upgrade_schema upgrade_config upgrade upgrade_sqlradius);
15
16 my $start = time;
17
18 die "Not running uid freeside!" unless checkeuid();
19
20 getopts("dqrs");
21
22 $DEBUG = !$opt_q;
23 #$DEBUG = $opt_v;
24
25 $DRY_RUN = $opt_d;
26
27 my $user = shift or die &usage;
28 $FS::CurrentUser::upgrade_hack = 1;
29 $FS::UID::callback_hack = 1;
30 my $dbh = adminsuidsetup($user);
31 $FS::UID::callback_hack = 0;
32
33 if ( driver_name =~ /^mysql/i ) { #until 0.39 is required above
34   eval "use DBIx::DBSchema 0.39;";
35   die $@ if $@;
36 }
37
38 #needs to match FS::Schema...
39 my $dbdef_file = "%%%FREESIDE_CONF%%%/dbdef.". datasrc;
40
41 dbdef_create($dbh, $dbdef_file);
42
43 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
44 reload_dbdef($dbdef_file);
45
46 warn "Upgrade startup completed in ". (time-$start). " seconds\n"; # if $DEBUG;
47 $start = time;
48
49 #$DBIx::DBSchema::DEBUG = $DEBUG;
50 #$DBIx::DBSchema::Table::DEBUG = $DEBUG;
51 #$DBIx::DBSchema::Index::DEBUG = $DEBUG;
52
53 my @bugfix = ();
54
55 if (dbdef->table('cust_main')->column('agent_custid') && ! $opt_s) { 
56   push @bugfix,
57     "UPDATE cust_main SET agent_custid = NULL where agent_custid = ''";
58
59   push @bugfix,
60     "UPDATE h_cust_main SET agent_custid = NULL where agent_custid = ''"
61       if (dbdef->table('h_cust_main')); 
62 }
63
64 if ( dbdef->table('cgp_rule_condition') &&
65      dbdef->table('cgp_rule_condition')->column('condition') 
66    )
67 {
68   push @bugfix,
69    "ALTER TABLE ${_}cgp_rule_condition RENAME COLUMN condition TO conditionname"
70       for '', 'h_';
71
72 }
73
74 if ( dbdef->table('areacode') and
75      dbdef->table('areacode')->primary_key eq 'code' )
76 {
77   if ( driver_name =~ /^mysql/i ) {
78     push @bugfix, 
79       'ALTER TABLE areacode DROP PRIMARY KEY',
80       'ALTER TABLE areacode ADD COLUMN (areanum int auto_increment primary key)';
81   }
82   else {
83     push @bugfix, 'ALTER TABLE areacode DROP CONSTRAINT areacode_pkey';
84   }
85 }
86
87 # RT required field flag
88 # for consistency with RT schema: mysql is in CamelCase,
89 # pg is in lowercase, and they use different data types.
90 my ($t, $creq, $cdis) = 
91   map { driver_name =~ /^mysql/i ? $_ : lc($_) }
92   ('CustomFields','Required','Disabled');
93
94 if ( dbdef->table($t) && 
95      ! dbdef->table($t)->column($creq) ) {
96   push @bugfix,
97     "ALTER TABLE $t ADD COLUMN $creq ".
98     dbdef->table($t)->column($cdis)->type .
99     ' NOT NULL DEFAULT 0';
100 }
101
102 if ( $DRY_RUN ) {
103   print
104     join(";\n", @bugfix ). ";\n";
105 } elsif ( @bugfix ) {
106
107   foreach my $statement ( @bugfix ) {
108     warn "$statement\n";
109     $dbh->do( $statement )
110       or die "Error: ". $dbh->errstr. "\n executing: $statement";
111   }
112
113   upgrade_schema();
114
115   dbdef_create($dbh, $dbdef_file);
116   delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
117   reload_dbdef($dbdef_file);
118
119 }
120
121 #you should have run fs-migrate-part_svc ages ago, when you upgraded
122 #from 1.3 to 1.4... if not, it needs to be hooked into -upgrade here or
123 #you'll lose all the part_svc settings it migrates to part_svc_column
124
125 my @statements = dbdef->sql_update_schema( dbdef_dist(datasrc),
126                                            $dbh,
127                                            { 'nullify_default' => 1, },
128                                          );
129
130 #### NEW CUSTOM FIELDS:
131 # 1. prevent new custom field columns from being dropped by upgrade
132 # 2. migrate old virtual fields to real fields (new custom fields)
133 ####
134 my $cfsth = $dbh->prepare("SELECT * FROM part_virtual_field") 
135                                                          or die $dbh->errstr;
136 $cfsth->execute or die $cfsth->errstr;
137 my $cf; 
138 while ( $cf = $cfsth->fetchrow_hashref ) {
139     my $tbl = $cf->{'dbtable'};
140     my $name = $cf->{'name'};
141     @statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
142                                                                     @statements;
143     push @statements, 
144         "ALTER TABLE $tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
145      unless (dbdef->table($tbl) && dbdef->table($tbl)->column("cf_$name"));
146     push @statements, 
147         "ALTER TABLE h_$tbl ADD COLUMN cf_$name varchar(".$cf->{'length'}.")"
148      unless (dbdef->table("h_$tbl") && dbdef->table("h_$tbl")->column("cf_$name"));
149 }
150 warn "Custom fields schema upgrade completed";
151
152 @statements = 
153   grep { $_ !~ /^CREATE +INDEX +h_queue/i } #useless, holds up queue insertion
154        @statements;
155
156 unless ( driver_name =~ /^mysql/i ) {
157   #not necessary under non-mysql, takes forever on big db
158   @statements =
159     grep { $_ !~ /^ *ALTER +TABLE +h_queue +ALTER +COLUMN +job +TYPE +varchar\(512\) *$/i }
160          @statements;
161 }
162
163 if ( $DRY_RUN ) {
164   print
165     join(";\n", @statements ). ";\n";
166   exit;
167 } else {
168   foreach my $statement ( @statements ) {
169     warn "$statement\n";
170     $dbh->do( $statement )
171       or die "Error: ". $dbh->errstr. "\n executing: $statement";
172   }
173
174 #  warn "Pre-schema change upgrades completed in ". (time-$start). " seconds\n"; # if $DEBUG;
175 #  $start = time;
176
177 #  dbdef->update_schema( dbdef_dist(datasrc), $dbh );
178 }
179
180 warn "Schema upgrade completed in ". (time-$start). " seconds\n"; # if $DEBUG;
181 $start = time;
182
183 my $hashref = {};
184 $hashref->{dry_run} = 1 if $DRY_RUN;
185 $hashref->{debug} = 1 if $DEBUG && $DRY_RUN;
186 prune_applications($hashref) unless $opt_s;
187
188 warn "Application pruning completed in ". (time-$start). " seconds\n"; # if $DEBUG;
189 $start = time;
190
191 print "\n" if $DRY_RUN;
192
193 if ( $dbh->{Driver}->{Name} =~ /^mysql/i && ! $opt_s ) {
194
195   foreach my $table (qw( svc_acct svc_phone )) {
196
197     my $sth = $dbh->prepare(
198       "SELECT COUNT(*) FROM duplicate_lock WHERE lockname = '$table'"
199     ) or die $dbh->errstr;
200
201     $sth->execute or die $sth->errstr;
202
203     unless ( $sth->fetchrow_arrayref->[0] ) {
204
205       $sth = $dbh->prepare(
206         "INSERT INTO duplicate_lock ( lockname ) VALUES ( '$table' )"
207       ) or die $dbh->errstr;
208
209       $sth->execute or die $sth->errstr;
210
211     }
212
213   }
214
215   warn "Duplication lock creation completed in ". (time-$start). " seconds\n"; # if $DEBUG;
216   $start = time;
217
218 }
219
220 $dbh->commit or die $dbh->errstr;
221
222 dbdef_create($dbh, $dbdef_file);
223
224 $dbh->disconnect or die $dbh->errstr;
225
226 delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
227 $FS::UID::AutoCommit = 0;
228 $FS::UID::callback_hack = 1;
229 $dbh = adminsuidsetup($user);
230 $FS::UID::callback_hack = 0;
231 unless ( $DRY_RUN || $opt_s ) {
232   my $dir = "%%%FREESIDE_CONF%%%/conf.". datasrc;
233   if (!scalar(qsearch('conf', {}))) {
234     my $error = FS::Conf::init_config($dir);
235     if ($error) {
236       warn "CONFIGURATION UPGRADE FAILED\n";
237       $dbh->rollback or die $dbh->errstr;
238       die $error;
239     }
240   }
241 }
242 $dbh->commit or die $dbh->errstr;
243 $dbh->disconnect or die $dbh->errstr;
244
245 $FS::UID::AutoCommit = 1;
246
247 $dbh = adminsuidsetup($user);
248
249 warn "Re-initialization with updated schema completed in ". (time-$start). " seconds\n"; # if $DEBUG;
250 $start = time;
251
252 #### NEW CUSTOM FIELDS:
253 # 3. migrate old virtual field data to the new custom fields
254 ####
255 $cfsth = $dbh->prepare("SELECT * FROM virtual_field left join part_virtual_field using (vfieldpart)")
256                                                          or die $dbh->errstr;
257 $cfsth->execute or die $cfsth->errstr;
258 my @cfst;
259 while ( $cf = $cfsth->fetchrow_hashref ) {
260     my $tbl = $cf->{'dbtable'};
261     my $name = $cf->{'name'};
262     my $dtable = dbdef->table($tbl);
263     next unless $dtable && $dtable->primary_key; # XXX: warn first?
264     my $pkey = $dtable->primary_key;
265     next unless $dtable->column($pkey)->type =~ /int/i; # XXX: warn first?
266     push @cfst, "UPDATE $tbl set cf_$name = '".$cf->{'value'}."' WHERE $pkey = ".$cf->{'recnum'};
267     push @cfst, "DELETE FROM virtual_field WHERE vfieldnum = ".$cf->{'vfieldnum'};
268 }
269 foreach my $cfst ( @cfst ) {
270     warn "$cfst\n";
271     $dbh->do( $cfst )
272       or die "Error: ". $dbh->errstr. "\n executing: $cfst";
273 }
274 warn "Custom fields data upgrade completed";
275
276 upgrade_config()
277   unless $DRY_RUN || $opt_s;
278
279 $dbh->commit or die $dbh->errstr;
280
281 warn "Config updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
282 $start = time;
283
284 upgrade()
285   unless $DRY_RUN || $opt_s;
286
287 $dbh->commit or die $dbh->errstr;
288
289 warn "Table updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
290 $start = time;
291
292 upgrade_sqlradius()
293   unless $DRY_RUN || $opt_s || $opt_r;
294
295 warn "SQL RADIUS updates completed in ". (time-$start). " seconds\n"; # if $DEBUG;
296 $start = time;
297
298 $dbh->commit or die $dbh->errstr;
299 $dbh->disconnect or die $dbh->errstr;
300
301 warn "Final commit and disconnection completed in ". (time-$start). " seconds; upgrade done!\n"; # if $DEBUG;
302
303 ###
304
305 sub dbdef_create { # reverse engineer the schema from the DB and save to file
306   my( $dbh, $file ) = @_;
307   my $dbdef = new_native DBIx::DBSchema $dbh;
308   $dbdef->save($file);
309 }
310
311 sub usage {
312   die "Usage:\n  freeside-upgrade [ -d ] [ -r ] [ -s ] [ -q | -v ] user\n"; 
313 }
314
315 =head1 NAME
316
317 freeside-upgrade - Upgrades database schema for new freeside verisons.
318
319 =head1 SYNOPSIS
320
321   freeside-upgrade [ -d ] [ -r ] [ -s ] [ -q | -v ]
322
323 =head1 DESCRIPTION
324
325 Reads your existing database schema and updates it to match the current schema,
326 adding any columns or tables necessary.
327
328 Also performs other upgrade functions:
329
330 =over 4
331
332 =item Calls FS:: Misc::prune::prune_applications (probably unnecessary every upgrade, but simply won't find any records to change)
333
334 =item If necessary, moves your configuration information from the filesystem in /usr/local/etc/freeside/conf.<datasrc> to the database.
335
336 =back
337
338   [ -d ]: Dry run; output SQL statements (to STDOUT) only, but do not execute
339           them.
340
341   [ -q ]: Run quietly.  This may become the default at some point.
342
343   [ -r ]: Skip sqlradius updates.  Useful for occassions where the sqlradius
344           databases may be inaccessible.
345
346   [ -v ]: Run verbosely, sending debugging information to STDERR.  This is the
347           current default.
348
349   [ -s ]: Schema changes only.  Useful for Pg/slony slaves where the data
350           changes will be replicated from the Pg/slony master.
351
352 =head1 SEE ALSO
353
354 =cut
355