Perl-Datenbank-Upgradescripte auf Objektorientierung & strict umgestellt
[kivitendo-erp.git] / SL / DBUpgrade2.pm
1 package SL::DBUpgrade2;
2
3 use IO::File;
4 use List::MoreUtils qw(any);
5
6 use SL::Common;
7 use SL::DBUpgrade2::Base;
8 use SL::DBUtils;
9 use SL::Iconv;
10
11 use strict;
12
13 sub new {
14   my $package = shift;
15
16   return bless({}, $package)->init(@_);
17 }
18
19 sub init {
20   my ($self, %params) = @_;
21
22   if ($params{auth}) {
23     $params{path_suffix} = "-auth";
24     $params{schema}      = "auth.";
25   }
26
27   $params{path_suffix} ||= '';
28   $params{schema}      ||= '';
29   $params{path}          = "sql/" . $params{dbdriver} . "-upgrade2" . $params{path_suffix};
30
31   map { $self->{$_} = $params{$_} } keys %params;
32
33   return $self;
34 }
35
36 sub path {
37   $_[0]{path};
38 }
39
40 sub parse_dbupdate_controls {
41   $::lxdebug->enter_sub();
42
43   my ($self) = @_;
44
45   my $form   = $self->{form};
46   my $locale = $::locale;
47
48   local *IN;
49   my %all_controls;
50
51   my $path = $self->path;
52
53   foreach my $file_name (<$path/*.sql>, <$path/*.pl>) {
54     next unless (open(IN, $file_name));
55
56     my $file = $file_name;
57     $file =~ s|.*/||;
58
59     my $control = {
60       "priority" => 1000,
61       "depends"  => [],
62     };
63
64     while (<IN>) {
65       chomp();
66       next unless (/^(--|\#)\s*\@/);
67       s/^(--|\#)\s*\@//;
68       s/\s*$//;
69       next if ($_ eq "");
70
71       my @fields = split(/\s*:\s*/, $_, 2);
72       next unless (scalar(@fields) == 2);
73
74       if ($fields[0] eq "depends") {
75         push(@{$control->{"depends"}}, split(/\s+/, $fields[1]));
76       } else {
77         $control->{$fields[0]} = $fields[1];
78       }
79     }
80
81     next if ($control->{ignore});
82
83     $control->{charset} = 'UTF-8' if $file =~ m/\.pl$/;
84     $control->{charset} = $control->{charset} || $control->{encoding} || Common::DEFAULT_CHARSET;
85
86     if (!$control->{"tag"}) {
87       _control_error($form, $file_name, $locale->text("Missing 'tag' field.")) ;
88     }
89
90     if ($control->{"tag"} =~ /[^a-zA-Z0-9_\(\)\-]/) {
91       _control_error($form, $file_name, $locale->text("The 'tag' field must only consist of alphanumeric characters or the carachters - _ ( )"))
92     }
93
94     if (defined($all_controls{$control->{"tag"}})) {
95       _control_error($form, $file_name, sprintf($locale->text("More than one control file with the tag '%s' exist."), $control->{"tag"}))
96     }
97
98     if (!$control->{"description"}) {
99       _control_error($form, $file_name, sprintf($locale->text("Missing 'description' field."))) ;
100     }
101
102     $control->{"priority"}  *= 1;
103     $control->{"priority"} ||= 1000;
104     $control->{"file"}       = $file;
105
106     delete @{$control}{qw(depth applied)};
107
108     $all_controls{$control->{"tag"}} = $control;
109
110     close(IN);
111   }
112
113   foreach my $control (values(%all_controls)) {
114     foreach my $dependency (@{$control->{"depends"}}) {
115       _control_error($form, $control->{"file"}, sprintf($locale->text("Unknown dependency '%s'."), $dependency)) if (!defined($all_controls{$dependency}));
116     }
117
118     map({ $_->{"loop"} = 0; } values(%all_controls));
119     _check_for_loops($form, $control->{"file"}, \%all_controls, $control->{"tag"});
120   }
121
122   map({ _dbupdate2_calculate_depth(\%all_controls, $_->{"tag"}) }
123       values(%all_controls));
124
125   $self->{all_controls} = \%all_controls;
126
127   $::lxdebug->leave_sub();
128
129   return $self;
130 }
131
132 sub process_query {
133   $::lxdebug->enter_sub();
134
135   my ($self, $dbh, $filename, $version_or_control, $db_charset) = @_;
136
137   my $form  = $self->{form};
138   my $fh    = IO::File->new($filename, "r") or $form->error("$filename : $!\n");
139   my $query = "";
140   my $sth;
141   my @quote_chars;
142
143   my $file_charset = Common::DEFAULT_CHARSET;
144   while (<$fh>) {
145     last if !/^--/;
146     next if !/^--\s*\@(?:charset|encoding):\s*(.+)/;
147     $file_charset = $1;
148     last;
149   }
150   $fh->seek(0, SEEK_SET);
151
152   $db_charset ||= Common::DEFAULT_CHARSET;
153
154   $dbh->begin_work();
155
156   while (<$fh>) {
157     $_ = SL::Iconv::convert($file_charset, $db_charset, $_);
158
159     # Remove DOS and Unix style line endings.
160     chomp;
161
162     # remove comments
163     s/--.*$//;
164
165     for (my $i = 0; $i < length($_); $i++) {
166       my $char = substr($_, $i, 1);
167
168       # Are we inside a string?
169       if (@quote_chars) {
170         if ($char eq $quote_chars[-1]) {
171           pop(@quote_chars);
172         } elsif (length $quote_chars[-1] > 1
173              &&  substr($_, $i, length $quote_chars[-1]) eq $quote_chars[-1]) {
174           $i   += length($quote_chars[-1]) - 1;
175           $char = $quote_chars[-1];
176           pop(@quote_chars);
177         }
178         $query .= $char;
179
180       } else {
181         my ($tag, $tag_end);
182         if (($char eq "'") || ($char eq "\"")) {
183           push(@quote_chars, $char);
184
185         } elsif ($char eq '$'                                            # start of dollar quoting
186              && ($tag_end  = index($_, '$', $i + 1)) > -1                # ends on same line
187              && (do { $tag = substr($_, $i + 1, $tag_end - $i - 1); 1 }) # extract tag
188              &&  $tag      =~ /^ (?= [A-Za-z_] [A-Za-z0-9_]* | ) $/x) {  # tag is identifier
189           push @quote_chars, $char = '$' . $tag . '$';
190           $i = $tag_end;
191         } elsif ($char eq ";") {
192
193           # Query is complete. Send it.
194
195           $sth = $dbh->prepare($query);
196           if (!$sth->execute()) {
197             my $errstr = $dbh->errstr;
198             $sth->finish();
199             $dbh->rollback();
200             $form->dberror("The database update/creation did not succeed. " .
201                            "The file ${filename} containing the following " .
202                            "query failed:<br>${query}<br>" .
203                            "The error message was: ${errstr}<br>" .
204                            "All changes in that file have been reverted.");
205           }
206           $sth->finish();
207
208           $char  = "";
209           $query = "";
210         }
211
212         $query .= $char;
213       }
214     }
215
216     # Insert a space at the end of each line so that queries split
217     # over multiple lines work properly.
218     if ($query ne '') {
219       $query .= @quote_chars ? "\n" : ' ';
220     }
221   }
222
223   if (ref($version_or_control) eq "HASH") {
224     $dbh->do("INSERT INTO " . $self->{schema} . "schema_info (tag, login) VALUES (" . $dbh->quote($version_or_control->{"tag"}) . ", " . $dbh->quote($form->{"login"}) . ")");
225   } elsif ($version_or_control) {
226     $dbh->do("UPDATE defaults SET version = " . $dbh->quote($version_or_control));
227   }
228   $dbh->commit();
229
230   $fh->close();
231
232   $::lxdebug->leave_sub();
233 }
234
235 # Process a Perl script which updates the database.
236 # If the script returns 1 then the update was successful.
237 # Return code "2" means "needs more interaction; remove
238 # users/nologin and end current request".
239 # All other return codes are fatal errors.
240 sub process_perl_script {
241   $::lxdebug->enter_sub();
242
243   my ($self, $dbh, $filename, $version_or_control, $db_charset) = @_;
244
245   $dbh->begin_work;
246
247   # setup dbup_ export vars & run script
248   my %dbup_myconfig = map { ($_ => $::form->{$_}) } qw(dbname dbuser dbpasswd dbhost dbport dbconnect);
249   my $result        = SL::DBUpgrade2::Base::execute_script(
250     file_name => $filename,
251     tag       => $version_or_control->{tag},
252     dbh       => $dbh,
253     locale    => $::locale,
254     myconfig  => \%dbup_myconfig,
255   );
256
257   if (1 != $result) {
258     $dbh->rollback();
259     $dbh->disconnect();
260   }
261
262   if (!defined($result)) {
263     print $::form->parse_html_template("dbupgrade/error", { file  => $filename, error => $@ });
264     ::end_of_request();
265   } elsif (1 != $result) {
266     unlink("users/nologin") if (2 == $result);
267     ::end_of_request();
268   }
269
270   if (ref($version_or_control) eq "HASH") {
271     $dbh->do("INSERT INTO " . $self->{schema} . "schema_info (tag, login) VALUES (" . $dbh->quote($version_or_control->{tag}) . ", " . $dbh->quote($::form->{login}) . ")");
272   } elsif ($version_or_control) {
273     $dbh->do("UPDATE defaults SET version = " . $dbh->quote($version_or_control));
274   }
275   $dbh->commit();
276
277   $::lxdebug->leave_sub();
278 }
279
280 sub process_file {
281   my ($self, $dbh, $filename, $version_or_control, $db_charset) = @_;
282
283   if ($filename =~ m/sql$/) {
284     $self->process_query($dbh, $filename, $version_or_control, $db_charset);
285   } else {
286     $self->process_perl_script($dbh, $filename, $version_or_control, $db_charset);
287   }
288 }
289
290 sub update_available {
291   my ($self, $cur_version) = @_;
292
293   local *SQLDIR;
294
295   my $dbdriver = $self->{dbdriver};
296   opendir SQLDIR, "sql/${dbdriver}-upgrade" || error("", "sql/${dbdriver}-upgrade: $!");
297   my @upgradescripts = grep /${dbdriver}-upgrade-\Q$cur_version\E.*\.(sql|pl)$/, readdir SQLDIR;
298   closedir SQLDIR;
299
300   return ($#upgradescripts > -1);
301 }
302
303 sub update2_available {
304   $::lxdebug->enter_sub();
305
306   my ($self, $dbh) = @_;
307
308   map { $_->{applied} = 0; } values %{ $self->{all_controls} };
309
310   my $sth = $dbh->prepare(qq|SELECT tag FROM | . $self->{schema} . qq|schema_info|);
311   if ($sth->execute) {
312     while (my ($tag) = $sth->fetchrow_array) {
313       $self->{all_controls}->{$tag}->{applied} = 1 if defined $self->{all_controls}->{$tag};
314     }
315   }
316   $sth->finish();
317
318   my $needs_update = any { !$_->{applied} } values %{ $self->{all_controls} };
319
320   $::lxdebug->leave_sub();
321
322   return $needs_update;
323 }
324
325 sub unapplied_upgrade_scripts {
326   my ($self, $dbh) = @_;
327
328   my @all_scripts = map { $_->{applied} = 0; $_ } $self->sort_dbupdate_controls;
329
330   my $query = qq|SELECT tag FROM | . $self->{schema} . qq|schema_info|;
331   my $sth   = $dbh->prepare($query);
332   $sth->execute || $self->{form}->dberror($query);
333   while (my ($tag) = $sth->fetchrow_array()) {
334     $self->{all_controls}->{$tag}->{applied} = 1 if defined $self->{all_controls}->{$tag};
335   }
336   $sth->finish;
337
338   return grep { !$_->{applied} } @all_scripts;
339 }
340
341 sub apply_admin_dbupgrade_scripts {
342   my ($self, $called_from_admin) = @_;
343
344   return 0 if !$self->{auth};
345
346   my $dbh               = $::auth->dbconnect;
347   my @unapplied_scripts = $self->unapplied_upgrade_scripts($dbh);
348
349   return 0 if !@unapplied_scripts;
350
351   my $db_charset           = $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET;
352   $self->{form}->{login} ||= 'admin';
353
354   map { $_->{description} = SL::Iconv::convert($_->{charset}, $db_charset, $_->{description}) } values %{ $self->{all_controls} };
355
356   if ($called_from_admin) {
357     $self->{form}->{title} = $::locale->text('Dataset upgrade');
358     $self->{form}->header;
359   }
360
361   print $self->{form}->parse_html_template("dbupgrade/header", { dbname => $::auth->{DB_config}->{db} });
362
363   foreach my $control (@unapplied_scripts) {
364     $::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
365     print $self->{form}->parse_html_template("dbupgrade/upgrade_message2", $control);
366
367     $self->process_file($dbh, "sql/$self->{dbdriver}-upgrade2-auth/$control->{file}", $control, $db_charset);
368   }
369
370   print $self->{form}->parse_html_template("dbupgrade/footer", { is_admin => 1 }) if $called_from_admin;
371
372   return 1;
373 }
374
375 sub _check_for_loops {
376   my ($form, $file_name, $controls, $tag, @path) = @_;
377
378   push(@path, $tag);
379
380   my $ctrl = $controls->{$tag};
381
382   if ($ctrl->{"loop"} == 1) {
383     # Not done yet.
384     _control_error($form, $file_name, $::locale->text("Dependency loop detected:") . " " . join(" -> ", @path))
385
386   } elsif ($ctrl->{"loop"} == 0) {
387     # Not checked yet.
388     $ctrl->{"loop"} = 1;
389     map({ _check_for_loops($form, $file_name, $controls, $_, @path); } @{ $ctrl->{"depends"} });
390     $ctrl->{"loop"} = 2;
391   }
392 }
393
394 sub _control_error {
395   my ($form, $file_name, $message) = @_;
396
397   $form = $::form;
398   my $locale = $::locale;
399
400   $form->error(sprintf($locale->text("Error in database control file '%s': %s"), $file_name, $message));
401 }
402
403 sub _dbupdate2_calculate_depth {
404   $::lxdebug->enter_sub(2);
405
406   my ($tree, $tag) = @_;
407
408   my $node = $tree->{$tag};
409
410   return $::lxdebug->leave_sub(2) if (defined($node->{"depth"}));
411
412   my $max_depth = 0;
413
414   foreach $tag (@{$node->{"depends"}}) {
415     _dbupdate2_calculate_depth($tree, $tag);
416     my $value = $tree->{$tag}->{"depth"};
417     $max_depth = $value if ($value > $max_depth);
418   }
419
420   $node->{"depth"} = $max_depth + 1;
421
422   $::lxdebug->leave_sub(2);
423 }
424
425 sub sort_dbupdate_controls {
426   my $self = shift;
427
428   $self->parse_dbupdate_controls unless $self->{all_controls};
429
430   return sort { ($a->{depth} <=> $b->{depth}) || ($a->{priority} <=> $b->{priority}) || ($a->{tag} cmp $b->{tag}) } values %{ $self->{all_controls} };
431 }
432
433 1;
434 __END__
435
436 =pod
437
438 =encoding utf8
439
440 =head1 NAME
441
442 SL::DBUpgrade2 - Parse database upgrade files stored in
443 C<sql/Pg-upgrade2> and C<sql/Pg-upgrade2-auth> (and also in
444 C<SQL/Pg-upgrade>)
445
446 =head1 SYNOPSIS
447
448   use SL::User;
449   use SL::DBUpgrade2;
450
451   # Apply outstanding updates to the authentication database
452   my $scripts = SL::DBUpgrade2->new(
453     form     => $::form,
454     dbdriver => 'Pg',
455     auth     => 1
456   );
457   $scripts->apply_admin_dbupgrade_scripts(1);
458
459   # Apply updates to a user database
460   my $scripts = SL::DBUpgrade2->new(
461     form     => $::form,
462     dbdriver => $::form->{dbdriver},
463     auth     => 1
464   );
465   User->dbupdate2($form, $scripts->parse_dbupdate_controls);
466
467 =head1 OVERVIEW
468
469 Database upgrade files are used to upgrade the database structure and
470 content of both the authentication database and the user
471 databases. They're applied when a user logs in. As long as the
472 authentication database is not up to date users cannot log in in
473 general, and the admin has to log in first in order to get his
474 database updated.
475
476 Database scripts form a tree by specifying which upgrade file depends
477 on which other upgrade file. This means that such files are always
478 applied in a well-defined order.
479
480 Each script is run in a separate transaction. If a script fails the
481 current transaction is rolled back and the whole upgrade process is
482 stopped. The user/admin is required to fix the issue manually.
483
484 A list of applied upgrade scripts is maintained in a table called
485 C<schema_info> for the user database and C<auth.schema_info>) for the
486 authentication database. They contain the tags, the login name of the
487 user having applied the script and the timestamp when the script was
488 applied.
489
490 Database upgrade files come in two flavours: SQL files and Perl
491 files. For both there are control fields that determine the order in
492 which they're executed, what charset the scripts are written in
493 etc. The control fields are tag/value pairs contained in comments.
494
495 =head1 OLD UPGRADE FILES
496
497 The files in C<sql/Pg-upgrade> are so old that I don't bother
498 documenting them. They're handled by this class, too, but new files
499 are only created as C<Pg-upgrade2> files.
500
501 =head1 CONTROL FIELDS
502
503 =head2 SYNTAX
504
505 Control fields for Perl files:
506
507   # @tag1: value1
508   # @tag2: some more values
509   sub do_stuff {
510   }
511   1;
512
513 Control fields for SQL files:
514
515   -- @tag1: value1
516   -- @tag2: some more values
517   ALTER TABLE ...;
518
519 =head2 TAGS AND THEIR MEANING
520
521 The following tags are recognized:
522
523 =over 4
524
525 =item tag
526
527 The name for this file. The C<tag> is also used for dependency
528 resolution (see C<depends>).
529
530 This is mandatory.
531
532 =item description
533
534 A description presented to the user when the update is applied.
535
536 This is mandatory.
537
538 =item depends
539
540 A space-separated list of tags of scripts this particular script
541 depends on. All other upgrades listed in C<depends> will be applied
542 before the current one is applied.
543
544 =item charset
545 =item encoding
546
547 The charset this file uses. Defaults to C<ISO-8859-15> if
548 missing. Both terms are recognized.
549
550 =item priority
551
552 Ordering the scripts by their dependencies alone produces a lot of
553 groups of scripts that could be applied at the same time (e.g. if both
554 B and C depend only on A then B could be applied before C or the other
555 way around). This field determines the order inside such a
556 group. Scripts with lower priority fields are executed before scripts
557 with higher priority fields.
558
559 If two scripts have equal priorities then their tag name decides.
560
561 The priority defaults to 1000.
562
563 =back
564
565 =head1 FUNCTIONS
566
567 =over 4
568
569 =item C<apply_admin_dbupgrade_scripts $called_from_admin>
570
571 Applies all unapplied upgrade files to the authentication/admin
572 database. The parameter C<$called_from_admin> should be truish if the
573 function is called from the web interface and falsish if it's called
574 from e.g. a command line script like C<scripts/dbupgrade2_tool.pl>.
575
576 =item C<init %params>
577
578 Initializes the object. Is called directly from L<new> and should not
579 be called again.
580
581 =item C<new %params>
582
583 Creates a new object. Possible parameters are:
584
585 =over 4
586
587 =item path
588
589 Path to the upgrade files to parse. Required.
590
591 =item form
592
593 C<SL::Form> object to use. Required.
594
595 =item dbdriver
596
597 Name of the database driver. Currently only C<Pg> for PostgreSQL is
598 supported.
599
600 =item auth
601
602 Optional parameter defaulting to 0. If trueish then the scripts read
603 are the ones applying to the authentication database.
604
605 =back
606
607 =item C<parse_dbupdate_controls>
608
609 Parses all files located in C<path> (see L<new>), ananlyzes their
610 control fields, builds the tree, and signals errors if control fields
611 are missing/wrong (e.g. a tag name listed in C<depends> is not
612 found). Sets C<$Self-&gt;{all_controls}> to the list of database
613 scripts.
614
615 =item C<process_file $dbh, $filename, $version_or_control, $db_charset>
616
617 Applies a single database upgrade file. Calls L<process_perl_script>
618 for Perl update files and C<process_query> for SQL update
619 files. Requires an open database handle(C<$dbh>), the file name
620 (C<$filename>), a hash structure of the file's control fields as
621 produced by L<parse_dbupdate_controls> (C<$version_or_control>) and
622 the database charset (for on-the-fly charset recoding of the script if
623 required, C<$db_charset>).
624
625 Returns the result of the actual function called.
626
627 =item C<process_perl_script $dbh, $filename, $version_or_control, $db_charset>
628
629 Applies a single Perl database upgrade file. Requires an open database
630 handle(C<$dbh>), the file name (C<$filename>), a hash structure of the
631 file's control fields as produced by L<parse_dbupdate_controls>
632 (C<$version_or_control>) and the database charset (for on-the-fly
633 charset recoding of the script if required, C<$db_charset>).
634
635 Perl scripts are executed via L<eval>. If L<eval> returns falsish then
636 an error is expected. There are two special return values: If the
637 script returns C<1> then the update was successful. Return code C<2>
638 means "needs more interaction from the user; remove users/nologin and
639 end current upgrade process". All other return codes are fatal errors.
640
641 Inside the Perl script several local variables exist that can be used:
642
643 =over 4
644
645 =item $dbup_locale
646
647 A locale object for translating messages
648
649 =item $dbh
650
651 The database handle (inside a transaction).
652
653 =item $::form
654
655 The global C<SL::Form> object.
656
657 =back
658
659 A Perl script can actually implement queries that fail while
660 continueing the process by handling the transaction itself, e.g. with
661 the following function:
662
663   sub do_query {
664     my ($query, $may_fail) = @_;
665
666     if (!$dbh->do($query)) {
667       die($dbup_locale->text("Database update error:") . "<br>$msg<br>" . $DBI::errstr) unless $may_fail;
668       $dbh->rollback();
669       $dbh->begin_work();
670     }
671   }
672
673 =item C<process_query $dbh, $filename, $version_or_control, $db_charset>
674
675 Applies a single SQL database upgrade file. Requires an open database
676 handle(C<$dbh>), the file name (C<$filename>), a hash structure of the
677 ofile's control fields as produced by L<parse_dbupdate_controls>
678 (C<$version_or_control>) and the database charset (for on-the-fly
679 charset recoding of the script if required, C<$db_charset>).
680
681 =item C<sort_dbupdate_controls>
682
683 Sorts the database upgrade scripts according to their C<tag> and
684 C<priority> control fields. Returns a list of their hash
685 representations that can be applied in order.
686
687 =item C<unapplied_upgrade_scripts $dbh>
688
689 Returns a list if upgrade scripts (their internal hash representation)
690 that haven't been applied to a database yet. C<$dbh> is an open handle
691 to the database that is checked.
692
693 Requires that the scripts have been parsed.
694
695 =item C<update2_available $dbh>
696
697 Returns trueish if at least one upgrade script hasn't been applied to
698 a database yet. C<$dbh> is an open handle to the database that is
699 checked.
700
701 Requires that the scripts have been parsed.
702
703 =back
704
705 =head1 BUGS
706
707 Nothing here yet.
708
709 =head1 AUTHOR
710
711 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
712
713 =cut