Bug 24883: Move to a flat array with all values to an array of arrayref
[srvgit] / 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 $all_languages = shift;
316     my @sql_list = @_;
317
318     my $lang;
319     my %hashlevel;
320     my @fnames = sort {
321         my @aa = split /\/|\\/, ($a);
322         my @bb = split /\/|\\/, ($b);
323         $aa[-1] cmp $bb[-1]
324     } @sql_list;
325     my $request = $self->{'dbh'}->prepare( "SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'" );
326     $request->execute;
327     my ($systempreference) = $request->fetchrow;
328     $systempreference = '' unless defined $systempreference; # avoid warning
329
330     my $global_mandatory_dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/mandatory";
331
332     # Make sure some stuffs are loaded first
333     unshift(@fnames, C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/sysprefs.sql");
334     unshift(@fnames,
335         "$global_mandatory_dir/subtag_registry.sql",
336         "$global_mandatory_dir/auth_val_cat.sql",
337         "$global_mandatory_dir/message_transport_types.sql",
338         "$global_mandatory_dir/sample_notices_message_attributes.sql",
339         "$global_mandatory_dir/sample_notices_message_transports.sql",
340         "$global_mandatory_dir/keyboard_shortcuts.sql",
341     );
342
343     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userflags.sql";
344     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userpermissions.sql";
345     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/audio_alerts.sql";
346     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_offset_types.sql";
347     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_credit_types.sql";
348     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_debit_types.sql";
349     foreach my $file (@fnames) {
350         #      warn $file;
351         undef $/;
352         my $error = $self->load_sql($file);
353         my @file = split qr(\/|\\), $file;
354         $lang = $file[ scalar(@file) - 3 ] unless ($lang);
355         my $level = $file[ scalar(@file) - 2 ];
356         unless ($error) {
357             $systempreference .= "$file[scalar(@file)-1]|"
358               unless ( index( $systempreference, $file[ scalar(@file) - 1 ] ) >= 0 );
359         }
360
361         #Bulding here a hierarchy to display files by level.
362         push @{ $hashlevel{$level} },
363           { "fwkname" => $file[ scalar(@file) - 1 ], "error" => $error };
364     }
365
366     #systempreference contains an ending |
367     chop $systempreference;
368     my @list;
369     map { push @list, { "level" => $_, "fwklist" => $hashlevel{$_} } } keys %hashlevel;
370     my $fwk_language;
371     for my $each_language (@$all_languages) {
372
373         #       warn "CODE".$each_language->{'language_code'};
374         #       warn "LANG:".$lang;
375         if ( $lang eq $each_language->{'language_code'} ) {
376             $fwk_language = $each_language->{language_locale_name};
377         }
378     }
379     my $updateflag =
380       $self->{'dbh'}->do(
381         "UPDATE systempreferences set value=\"$systempreference\" where variable='FrameworksLoaded'"
382       );
383
384     unless ( $updateflag == 1 ) {
385         my $string =
386             "INSERT INTO systempreferences (value, variable, explanation, type) VALUES (\"$systempreference\",'FrameworksLoaded','Frameworks loaded through webinstaller','choice')";
387         my $rq = $self->{'dbh'}->prepare($string);
388         $rq->execute;
389     }
390     return ($fwk_language, \@list);
391 }
392
393 =head2 set_marcflavour_syspref
394
395   $installer->set_marcflavour_syspref($marcflavour);
396
397 Set the 'marcflavour' system preference.  The incoming
398 C<$marcflavour> references to a subdirectory of
399 installer/data/$dbms/$lang/marcflavour, and is
400 normalized to MARC21, UNIMARC or NORMARC.
401
402 FIXME: this method assumes that the MARC flavour will be either
403 MARC21, UNIMARC or NORMARC.
404
405 =cut
406
407 sub set_marcflavour_syspref {
408     my $self = shift;
409     my $marcflavour = shift;
410
411     # we can have some variants of marc flavour, by having different directories, like : unimarc_small and unimarc_full, for small and complete unimarc frameworks.
412     # marc_cleaned finds the marcflavour, without the variant.
413     my $marc_cleaned = 'MARC21';
414     $marc_cleaned = 'UNIMARC' if $marcflavour =~ /unimarc/i;
415     $marc_cleaned = 'NORMARC' if $marcflavour =~ /normarc/i;
416     my $request =
417         $self->{'dbh'}->prepare(
418           "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');"
419         );
420     $request->execute;
421 }
422
423 =head2 set_version_syspref
424
425   $installer->set_version_syspref();
426
427 Set or update the 'Version' system preference to the current
428 Koha software version.
429
430 =cut
431
432 sub set_version_syspref {
433     my $self = shift;
434
435     my $kohaversion = Koha::version();
436     # remove the 3 last . to have a Perl number
437     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
438     if (C4::Context->preference('Version')) {
439         warn "UPDATE Version";
440         my $finish=$self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='Version'");
441         $finish->execute($kohaversion);
442     } else {
443         warn "INSERT Version";
444         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')");
445         $finish->execute($kohaversion);
446     }
447     C4::Context->clear_syspref_cache();
448 }
449
450 =head2 set_languages_syspref
451
452   $installer->set_languages_syspref();
453
454 Add the installation language to 'language' and 'opaclanguages' system preferences
455 if different from 'en'
456
457 =cut
458
459 sub set_languages_syspref {
460     my $self     = shift;
461     my $language = shift;
462
463     return if ( not $language or $language eq 'en' );
464
465     warn "UPDATE Languages";
466     # intranet
467     my $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='language'");
468     $pref->execute("en,$language");
469     # opac
470     $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='opaclanguages'");
471     $pref->execute("en,$language");
472
473     C4::Context->clear_syspref_cache();
474 }
475
476 =head2 load_sql
477
478   my $error = $installer->load_sql($filename);
479
480 Runs the specified input file using a sql loader DBIx::RunSQL, or a yaml loader
481 Returns any strings sent to STDERR
482
483 # FIXME This should be improved: sometimes the caller and load_sql warn the same
484 error.
485
486 =cut
487
488 sub process_yml_table {
489     my ($table) = @_;
490     my $table_name   = ( keys %$table )[0];                          # table name
491     my @rows         = @{ $table->{$table_name}->{rows} };           #
492     my @columns      = ( sort keys %{$rows[0]} );                    # column names
493     my $fields       = join ",", map{sprintf("`%s`", $_)} @columns;  # idem, joined
494     my $query        = "INSERT INTO $table_name ( $fields ) VALUES ";
495     my @multiline    = @{ $table->{$table_name}->{'multiline'} };    # to check multiline values;
496     my $placeholders = '(' . join ( ",", map { "?" } @columns ) . ')'; # '(?,..,?)' string
497     my @values;
498     foreach my $row ( @rows ) {
499         push @values, [ map {
500                         my $col = $_;
501                         ( @multiline and grep { $_ eq $col } @multiline )
502                         ? join "\r\n", @{$row->{$col}}                # join multiline values
503                         : $row->{$col};
504                      } @columns ];
505     }
506     return { query => $query, placeholders => $placeholders, values => \@values };
507 }
508
509 sub load_sql {
510     my $self = shift;
511     my $filename = shift;
512     my $error;
513
514     my $dbh = $self->{ dbh };
515
516     my $dup_stderr;
517     do {
518         local *STDERR;
519         open STDERR, ">>", \$dup_stderr;
520
521         if ( $filename =~ /sql$/ ) {                                                        # SQL files
522             eval {
523                 DBIx::RunSQL->run_sql_file(
524                     dbh     => $dbh,
525                     sql     => $filename,
526                 );
527             };
528         }
529         else {                                                                       # YAML files
530             eval {
531                 my $yaml         = LoadFile( $filename );                            # Load YAML
532                 for my $table ( @{ $yaml->{'tables'} } ) {
533                     my $query_info   = process_yml_table($table);
534                     my $query        = $query_info->{query};
535                     my $placeholders = $query_info->{placeholders};
536                     my $values       = $query_info->{values};
537                     # Doing only 1 INSERT query for the whole table
538                     my @all_rows_values = map { @$_ } @$values;
539                     $query .= join ', ', ( $placeholders ) x scalar @$values;
540                     $dbh->do( $query, undef, @all_rows_values );
541                 }
542                 for my $statement ( @{ $yaml->{'sql_statements'} } ) {               # extra SQL statements
543                     $dbh->do($statement);
544                 }
545             };
546         }
547         if ($@){
548             warn "Something went wrong loading file $filename ($@)";
549         }
550     };
551     #   errors thrown while loading installer data should be logged
552     if( $dup_stderr ) {
553         warn "C4::Installer::load_sql returned the following errors while attempting to load $filename:\n";
554         $error = $dup_stderr;
555     }
556
557     return $error;
558 }
559
560 =head2 get_file_path_from_name
561
562   my $filename = $installer->get_file_path_from_name('script_name');
563
564 searches through the set of known SQL scripts and finds the fully
565 qualified path name for the script that mathches the input.
566
567 returns undef if no match was found.
568
569
570 =cut
571
572 sub get_file_path_from_name {
573     my $self = shift;
574     my $partialname = shift;
575
576     my $lang = 'en'; # FIXME: how do I know what language I want?
577
578     my ($defaulted_to_en, $list) = $self->sample_data_sql_list($lang);
579     # warn( Data::Dumper->Dump( [ $list ], [ 'list' ] ) );
580
581     my @found;
582     foreach my $frameworklist ( @$list ) {
583         push @found, grep { $_->{'fwkfile'} =~ /$partialname$/ } @{$frameworklist->{'frameworks'}};
584     }
585
586     # warn( Data::Dumper->Dump( [ \@found ], [ 'found' ] ) );
587     if ( 0 == scalar @found ) {
588         return;
589     } elsif ( 1 < scalar @found ) {
590         warn "multiple results found for $partialname";
591         return;
592     } else {
593         return $found[0]->{'fwkfile'};
594     }
595
596 }
597
598 sub foreign_key_exists {
599     my ( $table_name, $constraint_name ) = @_;
600     my $dbh = C4::Context->dbh;
601     my (undef, $infos) = $dbh->selectrow_array(qq|SHOW CREATE TABLE $table_name|);
602     return $infos =~ m|CONSTRAINT `$constraint_name` FOREIGN KEY|;
603 }
604
605 sub index_exists {
606     my ( $table_name, $key_name ) = @_;
607     my $dbh = C4::Context->dbh;
608     my ($exists) = $dbh->selectrow_array(
609         qq|
610         SHOW INDEX FROM $table_name
611         WHERE key_name = ?
612         |, undef, $key_name
613     );
614     return $exists;
615 }
616
617 sub column_exists {
618     my ( $table_name, $column_name ) = @_;
619     return unless TableExists($table_name);
620     my $dbh = C4::Context->dbh;
621     my ($exists) = $dbh->selectrow_array(
622         qq|
623         SHOW COLUMNS FROM $table_name
624         WHERE Field = ?
625         |, undef, $column_name
626     );
627     return $exists;
628 }
629
630 sub TableExists { # Could be renamed table_exists for consistency
631     my $table = shift;
632     eval {
633                 my $dbh = C4::Context->dbh;
634                 local $dbh->{PrintError} = 0;
635                 local $dbh->{RaiseError} = 1;
636                 $dbh->do(qq{SELECT * FROM $table WHERE 1 = 0 });
637             };
638     return 1 unless $@;
639     return 0;
640 }
641
642
643 =head1 AUTHOR
644
645 C4::Installer is a refactoring of logic originally from installer/installer.pl, which was
646 originally written by Henri-Damien Laurant.
647
648 Koha Development Team <http://koha-community.org/>
649
650 Galen Charlton <galen.charlton@liblime.com>
651
652 =cut
653
654 1;