Bug 22789: (follow-up) Fix atomic update, GUI and more than one hold
[koha-ffzg.git] / C4 / Installer.pm
1 package C4::Installer;
2
3 # Copyright (C) 2008 LibLime
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Encode qw( encode is_utf8 );
23 use DBIx::RunSQL;
24 use YAML::Syck qw( LoadFile );
25 use C4::Context;
26 use DBI;
27 use Koha;
28
29 use vars qw(@ISA @EXPORT);
30 BEGIN {
31     require Exporter;
32     @ISA = qw( Exporter );
33     push @EXPORT, qw( foreign_key_exists index_exists column_exists TableExists);
34 };
35
36 =head1 NAME
37
38 C4::Installer
39
40 =head1 SYNOPSIS
41
42  use C4::Installer;
43  my $installer = C4::Installer->new();
44  my $all_languages = getAllLanguages();
45  my $error = $installer->load_db_schema();
46  my $list;
47  #fill $list with list of sql files
48  my ($fwk_language, $error_list) = $installer->load_sql_in_order($all_languages, @$list);
49  $installer->set_version_syspref();
50  $installer->set_marcflavour_syspref('MARC21');
51
52 =head1 DESCRIPTION
53
54 =cut
55
56 =head1 METHODS
57
58 =head2 new
59
60   my $installer = C4::Installer->new();
61
62 Creates a new installer.
63
64 =cut
65
66 sub new {
67     my $class = shift;
68
69     my $self = {};
70
71     # get basic information from context
72     $self->{'dbname'}   = C4::Context->config("database");
73     $self->{'dbms'}     = C4::Context->config("db_scheme") ? C4::Context->config("db_scheme") : "mysql";
74     $self->{'hostname'} = C4::Context->config("hostname");
75     $self->{'port'}     = C4::Context->config("port");
76     $self->{'user'}     = C4::Context->config("user");
77     $self->{'password'} = C4::Context->config("pass");
78     $self->{'tls'} = C4::Context->config("tls");
79     if( $self->{'tls'} && $self->{'tls'} eq 'yes' ) {
80         $self->{'ca'} = C4::Context->config('ca');
81         $self->{'cert'} = C4::Context->config('cert');
82         $self->{'key'} = C4::Context->config('key');
83         $self->{'tlsoptions'} = ";mysql_ssl=1;mysql_ssl_client_key=".$self->{key}.";mysql_ssl_client_cert=".$self->{cert}.";mysql_ssl_ca_file=".$self->{ca};
84         $self->{'tlscmdline'} =  " --ssl-cert ". $self->{cert} . " --ssl-key " . $self->{key} . " --ssl-ca ".$self->{ca}." "
85     }
86     $self->{'dbh'} = DBI->connect("DBI:$self->{dbms}:dbname=$self->{dbname};host=$self->{hostname}" .
87                                   ( $self->{port} ? ";port=$self->{port}" : "" ).
88                                   ( $self->{tlsoptions} ? $self->{tlsoptions} : ""),
89                                   $self->{'user'}, $self->{'password'});
90     $self->{'language'} = undef;
91     $self->{'marcflavour'} = undef;
92         $self->{'dbh'}->do('set NAMES "utf8"');
93     $self->{'dbh'}->{'mysql_enable_utf8'}=1;
94
95     bless $self, $class;
96     return $self;
97 }
98
99 =head2 marc_framework_sql_list
100
101   my ($defaulted_to_en, $list) = 
102      $installer->marc_framework_sql_list($lang, $marcflavour);
103
104 Returns in C<$list> a structure listing the filename, description, section,
105 and mandatory/optional status of MARC framework scripts available for C<$lang>
106 and C<$marcflavour>.
107
108 If the C<$defaulted_to_en> return value is true, no scripts are available
109 for language C<$lang> and the 'en' ones are returned.
110
111 =cut
112
113 sub marc_framework_sql_list {
114     my $self = shift;
115     my $lang = shift;
116     my $marcflavour = shift;
117
118     my $defaulted_to_en = 0;
119
120     undef $/;
121     my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang/marcflavour/".lc($marcflavour);
122     unless (opendir( MYDIR, $dir )) {
123         if ($lang eq 'en') {
124             warn "cannot open MARC frameworks directory $dir";
125         } else {
126             # if no translated MARC framework is available,
127             # default to English
128             $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/en/marcflavour/".lc($marcflavour);
129             opendir(MYDIR, $dir) or warn "cannot open English MARC frameworks directory $dir";
130             $defaulted_to_en = 1;
131         }
132     }
133     my @listdir = sort grep { !/^\.|marcflavour/ && -d "$dir/$_" } readdir(MYDIR);
134     closedir MYDIR;
135
136     my @fwklist;
137     my $request = $self->{'dbh'}->prepare("SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'");
138     $request->execute;
139     my ($frameworksloaded) = $request->fetchrow;
140     $frameworksloaded = '' unless defined $frameworksloaded; # avoid warning
141     my %frameworksloaded;
142     foreach ( split( /\|/, $frameworksloaded ) ) {
143         $frameworksloaded{$_} = 1;
144     }
145
146     foreach my $requirelevel (@listdir) {
147         opendir( MYDIR, "$dir/$requirelevel" );
148         my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.(sql|yml)$/ } readdir(MYDIR);
149         closedir MYDIR;
150         my %cell;
151         my @frameworklist;
152         map {
153             my ( $name, $ext ) = split /\./, $_;
154             my @lines;
155             if ( $ext =~ /yml/ ) {
156                 my $yaml = LoadFile("$dir/$requirelevel/$name\.$ext");
157                 @lines = map { Encode::decode('UTF-8', $_) } @{ $yaml->{'description'} };
158             } else {
159                 open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
160                 my $line = <$fh>;
161                 $line = Encode::encode('UTF-8', $line) unless ( Encode::is_utf8($line) );
162                 @lines = split /\n/, $line;
163             }
164             my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
165             push @frameworklist,
166               {
167                 'fwkname'        => $name,
168                 'fwkfile'        => "$dir/$requirelevel/$_",
169                 'fwkdescription' => \@lines,
170                 'checked'        => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
171                 'mandatory'      => $mandatory,
172               };
173         } @listname;
174         my @fwks =
175           sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
176
177         $cell{"frameworks"} = \@fwks;
178         $cell{"label"}      = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i)?'mandatory':'optional';
179         $cell{"code"}       = lc($requirelevel);
180         push @fwklist, \%cell;
181     }
182
183     return ($defaulted_to_en, \@fwklist);
184 }
185
186 =head2 sample_data_sql_list
187
188   my ($defaulted_to_en, $list) = $installer->sample_data_sql_list($lang);
189
190 Returns in C<$list> a structure listing the filename, description, section,
191 and mandatory/optional status of sample data scripts available for C<$lang>.
192 If the C<$defaulted_to_en> return value is true, no scripts are available
193 for language C<$lang> and the 'en' ones are returned.
194
195 =cut
196
197 sub sample_data_sql_list {
198     my $self = shift;
199     my $lang = shift;
200
201     my $defaulted_to_en = 0;
202
203     undef $/;
204     my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang";
205     unless (opendir( MYDIR, $dir )) {
206         if ($lang eq 'en') {
207             warn "cannot open sample data directory $dir";
208         } else {
209             # if no sample data is available,
210             # default to English
211             $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/en";
212             opendir(MYDIR, $dir) or warn "cannot open English sample data directory $dir";
213             $defaulted_to_en = 1;
214         }
215     }
216     my @listdir = sort grep { !/^\.|marcflavour/ && -d "$dir/$_" } readdir(MYDIR);
217     closedir MYDIR;
218
219     my @levellist;
220     my $request = $self->{'dbh'}->prepare("SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'");
221     $request->execute;
222     my ($frameworksloaded) = $request->fetchrow;
223     $frameworksloaded = '' unless defined $frameworksloaded; # avoid warning
224     my %frameworksloaded;
225     foreach ( split( /\|/, $frameworksloaded ) ) {
226         $frameworksloaded{$_} = 1;
227     }
228
229     foreach my $requirelevel (@listdir) {
230         opendir( MYDIR, "$dir/$requirelevel" );
231         my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.(sql|yml)$/ } readdir(MYDIR);
232         closedir MYDIR;
233         my %cell;
234         my @frameworklist;
235         map {
236             my ( $name, $ext ) = split /\./, $_;
237             my @lines;
238             if ( $ext =~ /yml/ ) {
239                 my $yaml = LoadFile("$dir/$requirelevel/$name\.$ext");
240                 @lines = map { Encode::decode('UTF-8', $_) } @{ $yaml->{'description'} };
241             } else {
242                 open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
243                 my $line = <$fh>;
244                 $line = Encode::encode('UTF-8', $line) unless ( Encode::is_utf8($line) );
245                 @lines = split /\n/, $line;
246             }
247             my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
248             push @frameworklist,
249               {
250                 'fwkname'        => $name,
251                 'fwkfile'        => "$dir/$requirelevel/$_",
252                 'fwkdescription' => \@lines,
253                 'checked'        => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
254                 'mandatory'      => $mandatory,
255               };
256         } @listname;
257         my @fwks = sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
258
259         $cell{"frameworks"} = \@fwks;
260         $cell{"label"}      = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i)?'mandatory':'optional';
261         $cell{"code"}       = lc($requirelevel);
262         push @levellist, \%cell;
263     }
264
265     return ($defaulted_to_en, \@levellist);
266 }
267
268 =head2 load_db_schema
269
270   my $error = $installer->load_db_schema();
271
272 Loads the SQL script that creates Koha's tables and indexes.  The
273 return value is a string containing error messages reported by the
274 load.
275
276 =cut
277
278 sub load_db_schema {
279     my $self = shift;
280
281     my $datadir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}";
282     my $error = $self->load_sql("$datadir/kohastructure.sql");
283     return $error;
284
285 }
286
287 =head2 load_sql_in_order
288
289   my ($fwk_language, $list) = $installer->load_sql_in_order($all_languages, @sql_list);
290
291 Given a list of SQL scripts supplied in C<@sql_list>, loads each of them
292 into the database and sets the FrameworksLoaded system preference to names
293 of the scripts that were loaded.
294
295 The SQL files are loaded in alphabetical order by filename (not including
296 directory path).  This means that dependencies among the scripts are to
297 be resolved by carefully naming them, keeping in mind that the directory name
298 does *not* currently count.
299
300 B<FIXME:> this is a rather delicate way of dealing with dependencies between
301 the install scripts.
302
303 The return value C<$list> is an arrayref containing a hashref for each
304 "level" or directory containing SQL scripts; the hashref in turns contains
305 a list of hashrefs containing a list of each script load and any error
306 messages associated with the loading of each script.
307
308 B<FIXME:> The C<$fwk_language> code probably doesn't belong and needs to be
309 moved to a different method.
310
311 =cut
312
313 sub load_sql_in_order {
314     my $self = shift;
315     my $langchoice = shift;
316     my $all_languages = shift;
317     my @sql_list = @_;
318
319     my $lang;
320     my %hashlevel;
321     my @fnames = sort {
322         my @aa = split /\/|\\/, ($a);
323         my @bb = split /\/|\\/, ($b);
324         $aa[-1] cmp $bb[-1]
325     } @sql_list;
326     my $request = $self->{'dbh'}->prepare( "SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'" );
327     $request->execute;
328     my ($systempreference) = $request->fetchrow;
329     $systempreference = '' unless defined $systempreference; # avoid warning
330
331     my $global_mandatory_dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/mandatory";
332
333     # Make sure some stuffs are loaded first
334     unshift(@fnames, C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/sysprefs.sql");
335     unshift(@fnames,
336         "$global_mandatory_dir/subtag_registry.sql",
337         "$global_mandatory_dir/auth_val_cat.sql",
338         "$global_mandatory_dir/message_transport_types.sql",
339         "$global_mandatory_dir/sample_notices_message_attributes.sql",
340         "$global_mandatory_dir/sample_notices_message_transports.sql",
341         "$global_mandatory_dir/keyboard_shortcuts.sql",
342     );
343
344     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userflags.sql";
345     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userpermissions.sql";
346     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/audio_alerts.sql";
347     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_offset_types.sql";
348     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_credit_types.sql";
349     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_debit_types.sql";
350     my $localization_file = C4::Context->config('intranetdir') .
351                             "/installer/data/$self->{dbms}/localization/$langchoice/custom.sql";
352     if ( $langchoice ne 'en' and -f $localization_file ) {
353         push @fnames, $localization_file;
354     }
355     foreach my $file (@fnames) {
356         #      warn $file;
357         undef $/;
358         my $error = $self->load_sql($file);
359         my @file = split qr(\/|\\), $file;
360         $lang = $file[ scalar(@file) - 3 ] unless ($lang);
361         my $level = ( $file =~ /(localization)/ ) ? $1 : $file[ scalar(@file) - 2 ];
362         unless ($error) {
363             $systempreference .= "$file[scalar(@file)-1]|"
364               unless ( index( $systempreference, $file[ scalar(@file) - 1 ] ) >= 0 );
365         }
366
367         #Bulding here a hierarchy to display files by level.
368         push @{ $hashlevel{$level} },
369           { "fwkname" => $file[ scalar(@file) - 1 ], "error" => $error };
370     }
371
372     #systempreference contains an ending |
373     chop $systempreference;
374     my @list;
375     map { push @list, { "level" => $_, "fwklist" => $hashlevel{$_} } } keys %hashlevel;
376     my $fwk_language;
377     for my $each_language (@$all_languages) {
378
379         #       warn "CODE".$each_language->{'language_code'};
380         #       warn "LANG:".$lang;
381         if ( $lang eq $each_language->{'language_code'} ) {
382             $fwk_language = $each_language->{language_locale_name};
383         }
384     }
385     my $updateflag =
386       $self->{'dbh'}->do(
387         "UPDATE systempreferences set value=\"$systempreference\" where variable='FrameworksLoaded'"
388       );
389
390     unless ( $updateflag == 1 ) {
391         my $string =
392             "INSERT INTO systempreferences (value, variable, explanation, type) VALUES (\"$systempreference\",'FrameworksLoaded','Frameworks loaded through webinstaller','choice')";
393         my $rq = $self->{'dbh'}->prepare($string);
394         $rq->execute;
395     }
396     return ($fwk_language, \@list);
397 }
398
399 =head2 set_marcflavour_syspref
400
401   $installer->set_marcflavour_syspref($marcflavour);
402
403 Set the 'marcflavour' system preference.  The incoming
404 C<$marcflavour> references to a subdirectory of
405 installer/data/$dbms/$lang/marcflavour, and is
406 normalized to MARC21, UNIMARC or NORMARC.
407
408 FIXME: this method assumes that the MARC flavour will be either
409 MARC21, UNIMARC or NORMARC.
410
411 =cut
412
413 sub set_marcflavour_syspref {
414     my $self = shift;
415     my $marcflavour = shift;
416
417     # we can have some variants of marc flavour, by having different directories, like : unimarc_small and unimarc_full, for small and complete unimarc frameworks.
418     # marc_cleaned finds the marcflavour, without the variant.
419     my $marc_cleaned = 'MARC21';
420     $marc_cleaned = 'UNIMARC' if $marcflavour =~ /unimarc/i;
421     $marc_cleaned = 'NORMARC' if $marcflavour =~ /normarc/i;
422     my $request =
423         $self->{'dbh'}->prepare(
424           "INSERT IGNORE INTO `systempreferences` (variable,value,explanation,options,type) VALUES('marcflavour','$marc_cleaned','Define global MARC flavor (MARC21, UNIMARC or NORMARC) used for character encoding','MARC21|UNIMARC|NORMARC','Choice');"
425         );
426     $request->execute;
427 }
428
429 =head2 set_version_syspref
430
431   $installer->set_version_syspref();
432
433 Set or update the 'Version' system preference to the current
434 Koha software version.
435
436 =cut
437
438 sub set_version_syspref {
439     my $self = shift;
440
441     my $kohaversion = Koha::version();
442     # remove the 3 last . to have a Perl number
443     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
444     if (C4::Context->preference('Version')) {
445         warn "UPDATE Version";
446         my $finish=$self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='Version'");
447         $finish->execute($kohaversion);
448     } else {
449         warn "INSERT Version";
450         my $finish=$self->{'dbh'}->prepare("INSERT into systempreferences (variable,value,explanation) values ('Version',?,'The Koha database version. WARNING: Do not change this value manually, it is maintained by the webinstaller')");
451         $finish->execute($kohaversion);
452     }
453     C4::Context->clear_syspref_cache();
454 }
455
456 =head2 set_languages_syspref
457
458   $installer->set_languages_syspref();
459
460 Add the installation language to 'language' and 'opaclanguages' system preferences
461 if different from 'en'
462
463 =cut
464
465 sub set_languages_syspref {
466     my $self     = shift;
467     my $language = shift;
468
469     return if ( not $language or $language eq 'en' );
470
471     warn "UPDATE Languages";
472     # intranet
473     my $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='language'");
474     $pref->execute("en,$language");
475     # opac
476     $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='opaclanguages'");
477     $pref->execute("en,$language");
478
479     C4::Context->clear_syspref_cache();
480 }
481
482 =head2 process_yml_table
483
484   my $query_info   = $installer->process_yml_table($table);
485
486 Analyzes a table loaded in YAML format.
487 Returns the values required to build an insert statement.
488
489 =cut
490
491 sub process_yml_table {
492     my ($table) = @_;
493     my $table_name   = ( keys %$table )[0];                          # table name
494     my @rows         = @{ $table->{$table_name}->{rows} };           #
495     my @columns      = ( sort keys %{$rows[0]} );                    # column names
496     my $fields       = join ",", map{sprintf("`%s`", $_)} @columns;  # idem, joined
497     my $query        = "INSERT INTO $table_name ( $fields ) VALUES ";
498     my @multiline    = @{ $table->{$table_name}->{'multiline'} };    # to check multiline values;
499     my $placeholders = '(' . join ( ",", map { "?" } @columns ) . ')'; # '(?,..,?)' string
500     my @values;
501     foreach my $row ( @rows ) {
502         push @values, [ map {
503                         my $col = $_;
504                         ( @multiline and grep { $_ eq $col } @multiline )
505                         ? join "\r\n", @{$row->{$col}}                # join multiline values
506                         : $row->{$col};
507                      } @columns ];
508     }
509     return { query => $query, placeholders => $placeholders, values => \@values };
510 }
511
512 =head2 load_sql
513
514   my $error = $installer->load_sql($filename);
515
516 Runs the specified input file using a sql loader DBIx::RunSQL, or a yaml loader
517 Returns any strings sent to STDERR
518
519 # FIXME This should be improved: sometimes the caller and load_sql warn the same
520 error.
521
522 =cut
523
524 sub load_sql {
525     my $self = shift;
526     my $filename = shift;
527     my $error;
528
529     my $dbh = $self->{ dbh };
530
531     my $dup_stderr;
532     do {
533         local *STDERR;
534         open STDERR, ">>", \$dup_stderr;
535
536         if ( $filename =~ /sql$/ ) {                                                        # SQL files
537             eval {
538                 DBIx::RunSQL->run_sql_file(
539                     dbh     => $dbh,
540                     sql     => $filename,
541                 );
542             };
543         }
544         else {                                                                       # YAML files
545             eval {
546                 my $yaml         = LoadFile( $filename );                            # Load YAML
547                 for my $table ( @{ $yaml->{'tables'} } ) {
548                     my $query_info   = process_yml_table($table);
549                     my $query        = $query_info->{query};
550                     my $placeholders = $query_info->{placeholders};
551                     my $values       = $query_info->{values};
552                     # Doing only 1 INSERT query for the whole table
553                     my @all_rows_values = map { @$_ } @$values;
554                     $query .= join ', ', ( $placeholders ) x scalar @$values;
555                     $dbh->do( $query, undef, @all_rows_values );
556                 }
557                 for my $statement ( @{ $yaml->{'sql_statements'} } ) {               # extra SQL statements
558                     $dbh->do($statement);
559                 }
560             };
561         }
562         if ($@){
563             warn "Something went wrong loading file $filename ($@)";
564         }
565     };
566     #   errors thrown while loading installer data should be logged
567     if( $dup_stderr ) {
568         warn "C4::Installer::load_sql returned the following errors while attempting to load $filename:\n";
569         $error = $dup_stderr;
570     }
571
572     return $error;
573 }
574
575 =head2 get_file_path_from_name
576
577   my $filename = $installer->get_file_path_from_name('script_name');
578
579 searches through the set of known SQL scripts and finds the fully
580 qualified path name for the script that mathches the input.
581
582 returns undef if no match was found.
583
584
585 =cut
586
587 sub get_file_path_from_name {
588     my $self = shift;
589     my $partialname = shift;
590
591     my $lang = 'en'; # FIXME: how do I know what language I want?
592
593     my ($defaulted_to_en, $list) = $self->sample_data_sql_list($lang);
594     # warn( Data::Dumper->Dump( [ $list ], [ 'list' ] ) );
595
596     my @found;
597     foreach my $frameworklist ( @$list ) {
598         push @found, grep { $_->{'fwkfile'} =~ /$partialname$/ } @{$frameworklist->{'frameworks'}};
599     }
600
601     # warn( Data::Dumper->Dump( [ \@found ], [ 'found' ] ) );
602     if ( 0 == scalar @found ) {
603         return;
604     } elsif ( 1 < scalar @found ) {
605         warn "multiple results found for $partialname";
606         return;
607     } else {
608         return $found[0]->{'fwkfile'};
609     }
610
611 }
612
613 sub foreign_key_exists {
614     my ( $table_name, $constraint_name ) = @_;
615     my $dbh = C4::Context->dbh;
616     my (undef, $infos) = $dbh->selectrow_array(qq|SHOW CREATE TABLE $table_name|);
617     return $infos =~ m|CONSTRAINT `$constraint_name` FOREIGN KEY|;
618 }
619
620 sub index_exists {
621     my ( $table_name, $key_name ) = @_;
622     my $dbh = C4::Context->dbh;
623     my ($exists) = $dbh->selectrow_array(
624         qq|
625         SHOW INDEX FROM $table_name
626         WHERE key_name = ?
627         |, undef, $key_name
628     );
629     return $exists;
630 }
631
632 sub column_exists {
633     my ( $table_name, $column_name ) = @_;
634     return unless TableExists($table_name);
635     my $dbh = C4::Context->dbh;
636     my ($exists) = $dbh->selectrow_array(
637         qq|
638         SHOW COLUMNS FROM $table_name
639         WHERE Field = ?
640         |, undef, $column_name
641     );
642     return $exists;
643 }
644
645 sub TableExists { # Could be renamed table_exists for consistency
646     my $table = shift;
647     eval {
648                 my $dbh = C4::Context->dbh;
649                 local $dbh->{PrintError} = 0;
650                 local $dbh->{RaiseError} = 1;
651                 $dbh->do(qq{SELECT * FROM $table WHERE 1 = 0 });
652             };
653     return 1 unless $@;
654     return 0;
655 }
656
657
658 =head1 AUTHOR
659
660 C4::Installer is a refactoring of logic originally from installer/installer.pl, which was
661 originally written by Henri-Damien Laurant.
662
663 Koha Development Team <http://koha-community.org/>
664
665 Galen Charlton <galen.charlton@liblime.com>
666
667 =cut
668
669 1;