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