-
-
Notifications
You must be signed in to change notification settings - Fork 28
/
Copy pathDB.pm
2839 lines (2045 loc) · 74.9 KB
/
DB.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::DB;
use 5.10.1;
use Moo;
use DBI;
use DBIx::Connector;
use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::Install::Requirements;
use Bugzilla::Install::Util qw(install_string);
use Bugzilla::Version qw(vers_cmp);
use Bugzilla::Install::Util qw(install_string);
use Bugzilla::Install::Localconfig;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::DB::Schema;
use Bugzilla::Version;
use List::Util qw(max);
use Scalar::Util qw(weaken);
use Storable qw(dclone);
use English qw(-no_match_vars);
use Module::Runtime qw(require_module);
has 'connector' => (is => 'lazy', handles => [qw( dbh )]);
has 'model' => (is => 'lazy');
has [qw(dsn user pass attrs)] => (is => 'ro', required => 1);
around 'attrs' => sub {
my ($method, $self) = @_;
my $attrs = dclone($self->$method);
my $class = ref $self;
# This is only used by the DBIx::Class code DBIx::Connector does something
# different because the old Bugzilla code has its own ideas about
# transactions.
$attrs->{Callbacks}{connected} = sub {
my ($dbh, $dsn) = @_;
$class->on_dbi_connected(@_) if $class->can('on_dbi_connected');
return;
};
return $attrs;
};
# Install proxy methods to the DBI object.
# We can't use handles() as DBIx::Connector->dbh has to be called each
# time we need a DBI handle to ensure the connection is alive.
{
my @DBI_METHODS = qw(
begin_work column_info commit do errstr get_info last_insert_id ping prepare
primary_key quote_identifier rollback selectall_arrayref selectall_hashref
selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info
);
my $stash = Package::Stash->new(__PACKAGE__);
foreach my $method (@DBI_METHODS) {
my $symbol = '&' . $method;
$stash->add_symbol(
$symbol => sub {
my $self = shift;
my $wantarray = wantarray;
my $result = eval {
if ($wantarray) {
[$self->dbh->$method(@_)];
}
else {
[scalar $self->dbh->$method(@_)];
}
};
if (not defined $result) {
my $err = $@;
Carp::confess($err);
}
else {
return $wantarray ? @$result : $result->[0];
}
}
);
}
}
#####################################################################
# Constants
#####################################################################
use constant BLOB_TYPE => DBI::SQL_BLOB;
use constant ISOLATION_LEVEL => 'REPEATABLE READ';
# Set default values for what used to be the enum types. These values
# are no longer stored in localconfig. If we are upgrading from a
# Bugzilla with enums to a Bugzilla without enums, we use the
# enum values.
#
# The values that you see here are ONLY DEFAULTS. They are only used
# the FIRST time you run checksetup.pl, IF you are NOT upgrading from a
# Bugzilla with enums. After that, they are either controlled through
# the Bugzilla UI or through the DB.
use constant ENUM_DEFAULTS => {
bug_type => ['defect', 'enhancement', 'task', '--'],
bug_severity =>
['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', '--'],
priority => ["Highest", "High", "Normal", "Low", "Lowest", "--"],
op_sys => ["All", "Windows", "Mac OS", "Linux", "Other"],
rep_platform => ["All", "PC", "Macintosh", "Other"],
bug_status =>
["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"],
resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"],
};
# The character that means "OR" in a boolean fulltext search. If empty,
# the database doesn't support OR searches in fulltext searches.
# Used by Bugzilla::Bug::possible_duplicates.
use constant FULLTEXT_OR => '';
# These are used in regular expressions to mean "the start or end of a word".
#
# We don't use [[:<:]] and [[:>:]], even though they mean
# "start and end of a word" and are supported by both MySQL and PostgreSQL,
# because they don't work if your search starts or ends with a non-alphanumeric
# character, and there's a fair chance somebody will want to use the "word"
# search to search flags for something like "review+".
#
# We do use [:almum:] because it is supported by at least MySQL and
# PostgreSQL, and hopefully will get us as much Unicode support as possible,
# depending on how well the regexp engines of the various databases support
# Unicode.
use constant WORD_START => '(^|[^[:alnum:]])';
use constant WORD_END => '($|[^[:alnum:]])';
# On most databases, in order to drop an index, you have to first drop
# the foreign keys that use that index. However, on some databases,
# dropping the FK immediately before dropping the index causes problems
# and doesn't need to be done anyway, so those DBs set this to 0.
use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1;
#####################################################################
# Overridden Superclass Methods
#####################################################################
sub quote {
my $self = shift;
my $retval = $self->dbh->quote(@_);
return $retval;
}
#####################################################################
# Connection Methods
#####################################################################
sub connect_shadow {
state $shadow_dbh;
if ($shadow_dbh && $shadow_dbh->bz_in_transaction) {
FATAL("Somehow in a transaction at connection time");
$shadow_dbh->bz_rollback_transaction();
}
return $shadow_dbh if $shadow_dbh;
my $params = Bugzilla->params;
die "Tried to connect to non-existent shadowdb"
unless Bugzilla->get_param_with_override('shadowdb');
# Instead of just passing in a new hashref, we locally modify the
# values of "localconfig", because some drivers access it while
# connecting.
my $connect_params = dclone(Bugzilla->localconfig);
$connect_params->{db_host} = Bugzilla->get_param_with_override('shadowdbhost');
$connect_params->{db_name} = Bugzilla->get_param_with_override('shadowdb');
$connect_params->{db_port} = Bugzilla->get_param_with_override('shadowdbport');
$connect_params->{db_sock} = Bugzilla->get_param_with_override('shadowdbsock');
if ( Bugzilla->localconfig->shadowdb_user
&& Bugzilla->localconfig->shadowdb_pass)
{
$connect_params->{db_user} = Bugzilla->localconfig->shadowdb_user;
$connect_params->{db_pass} = Bugzilla->localconfig->shadowdb_pass;
}
return $shadow_dbh = _connect($connect_params);
}
sub connect_main {
state $main_dbh = _connect(Bugzilla->localconfig);
if ($main_dbh->bz_in_transaction) {
FATAL("Somehow in a transaction at connection time");
$main_dbh->bz_rollback_transaction();
}
return $main_dbh;
}
sub _connect {
my ($params) = @_;
my $driver = $params->{db_driver};
my $pkg_module = DB_MODULE->{lc($driver)}->{db};
# do the actual import
eval { require_module($pkg_module) }
|| die(
"'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@);
# instantiate the correct DB specific module
return $pkg_module->new($params);
}
sub _handle_error {
require Carp;
# Cut down the error string to a reasonable size
$_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
if length($_[0]) > 4000;
# BMO: stracktrace disabled:
# $_[0] = Carp::longmess($_[0]);
# BMO: catch long running query timeouts and translate into a sane message
#if ($_[0] =~ /Lost connection to MySQL server during query/) {
# warn(Carp::longmess($_[0]));
# $_[0] = "The database query took too long to complete and has been canceled.\n"
# . "(Lost connection to MySQL server during query)";
#}
#if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
# ThrowCodeError("db_error", { err_message => $_[0] });
#}
# keep tests happy
if (0) {
ThrowCodeError("db_error", {err_message => $_[0]});
}
return 0; # Now let DBI handle raising the error
}
sub bz_check_requirements {
my ($output) = @_;
my $lc = Bugzilla->localconfig;
my $db = DB_MODULE->{lc($lc->{db_driver})};
# Only certain values are allowed for $db_driver.
if (!defined $db) {
die "$lc->{db_driver} is not a valid choice for \$db_driver in"
. bz_locations()->{'localconfig'};
}
# We don't try to connect to the actual database if $db_check is
# disabled.
unless ($lc->{db_check}) {
print "\n" if $output;
return;
}
# And now check the version of the database server itself.
my $dbh = _get_no_db_connection();
$dbh->bz_check_server_version($db, $output);
print "\n" if $output;
}
sub bz_check_server_version {
my ($self, $db, $output) = @_;
my $sql_vers = $self->bz_server_version;
my $sql_want = $db->{db_version};
my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
my $sql_server = $db->{name};
if ($output) {
Bugzilla::Install::Requirements::_checking_for({
package => $sql_server,
wanted => $sql_want,
found => $sql_vers,
ok => $version_ok
});
}
# Check what version of the database server is installed and let
# the user know if the version is too old to be used with Bugzilla.
if (!$version_ok) {
die <<EOT;
Your $sql_server v$sql_vers is too old. Bugzilla requires version
$sql_want or later of $sql_server. Please download and install a
newer version.
EOT
}
# This is used by subclasses.
return $sql_vers;
}
# Note that this function requires that localconfig exist and
# be valid.
sub bz_create_database {
my $dbh;
# See if we can connect to the actual Bugzilla database.
my $conn_success = eval { $dbh = connect_main() };
my $db_name = Bugzilla->localconfig->db_name;
if (!$conn_success) {
$dbh = _get_no_db_connection();
print "Creating database $db_name...\n";
# Try to create the DB, and if we fail print a friendly error.
my $success = eval {
my @sql = $dbh->_bz_schema->get_create_database_sql($db_name);
# This ends with 1 because this particular do doesn't always
# return something.
$dbh->do($_) foreach @sql;
1;
};
if (!$success) {
my $error = $dbh->errstr || $@;
chomp($error);
die "The '$db_name' database could not be created.",
" The error returned was:\n\n $error\n\n", _bz_connect_error_reasons();
}
}
}
# A helper for bz_create_database and bz_check_requirements.
sub _get_no_db_connection {
my ($sql_server) = @_;
my $dbh;
my %connect_params = %{Bugzilla->localconfig};
$connect_params{db_name} = '';
my $conn_success = eval { $dbh = _connect(\%connect_params); };
if (!$conn_success) {
my $driver = $connect_params{db_driver};
my $sql_server = DB_MODULE->{lc($driver)}->{name};
# Can't use $dbh->errstr because $dbh is undef.
my $error = $DBI::errstr || $@;
chomp($error);
die "There was an error connecting to $sql_server:\n\n", " $error\n\n",
_bz_connect_error_reasons(), "\n";
}
return $dbh;
}
# Just a helper because we have to re-use this text.
# We don't use this in db_new because it gives away the database
# username, and db_new errors can show up on CGIs.
sub _bz_connect_error_reasons {
my $lc_file = bz_locations()->{'localconfig'};
my $lc = Bugzilla->localconfig;
my $db = DB_MODULE->{lc($lc->{db_driver})};
my $server = $db->{name};
return <<EOT;
This might have several reasons:
* $server is not running.
* $server is running, but there is a problem either in the
server configuration or the database access rights. Read the Bugzilla
Guide in the doc directory. The section about database configuration
should help.
* Your password for the '$lc->{db_user}' user, specified in \$db_pass, is
incorrect, in '$lc_file'.
* There is a subtle problem with Perl, DBI, or $server. Make
sure all settings in '$lc_file' are correct. If all else fails, set
'\$db_check' to 0.
EOT
}
# List of abstract methods we are checking the derived class implements
our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit sql_to_days
sql_date_format sql_date_math bz_explain
sql_group_concat);
# This overridden import method will check implementation of inherited classes
# for missing implementation of abstract methods
# See http://perlmonks.thepen.com/44265.html
sub import {
my $pkg = shift;
# do not check this module
if ($pkg ne __PACKAGE__) {
# make sure all abstract methods are implemented
foreach my $meth (@_abstract_methods) {
$pkg->can($meth) or die("Class $pkg does not define method $meth");
}
}
# Now we want to call our superclass implementation.
# If our superclass is Exporter, which is using caller() to find
# a namespace to populate, we need to adjust for this extra call.
# All this can go when we stop using deprecated functions.
my $is_exporter = $pkg->isa('Exporter');
$Exporter::ExportLevel++ if $is_exporter;
$pkg->SUPER::import(@_);
$Exporter::ExportLevel-- if $is_exporter;
}
sub sql_prefix_match {
my ($self, $column, $str) = @_;
my $must_escape = $str =~ s/([_%!])/!$1/g;
my $escape = $must_escape ? q/ESCAPE '!'/ : '';
my $quoted_str = $self->quote("$str%");
return "$column LIKE $quoted_str $escape";
}
sub sql_istrcmp {
my ($self, $left, $right, $op) = @_;
$op ||= "=";
return $self->sql_istring($left) . " $op " . $self->sql_istring($right);
}
sub sql_istring {
my ($self, $string) = @_;
return "LOWER($string)";
}
sub sql_iposition {
my ($self, $fragment, $text) = @_;
$fragment = $self->sql_istring($fragment);
$text = $self->sql_istring($text);
return $self->sql_position($fragment, $text);
}
sub sql_position {
my ($self, $fragment, $text) = @_;
return "POSITION($fragment IN $text)";
}
sub sql_group_by {
my ($self, $needed_columns, $optional_columns) = @_;
my $expression = "GROUP BY $needed_columns";
$expression .= ", " . $optional_columns if $optional_columns;
return $expression;
}
sub sql_string_concat {
my ($self, @params) = @_;
return '(' . join(' || ', @params) . ')';
}
sub sql_string_until {
my ($self, $string, $substring) = @_;
my $position = $self->sql_position($substring, $string);
return
"CASE WHEN $position != 0"
. " THEN SUBSTR($string, 1, $position - 1)"
. " ELSE $string END";
}
sub sql_in {
my ($self, $column_name, $in_list_ref, $negate) = @_;
return
" $column_name "
. ($negate ? "NOT " : "") . "IN ("
. join(',', @$in_list_ref) . ") ";
}
sub sql_fulltext_search {
my ($self, $column, $text) = @_;
# This is as close as we can get to doing full text search using
# standard ANSI SQL, without real full text search support. DB specific
# modules should override this, as this will be always much slower.
# make the string lowercase to do case insensitive search
my $lower_text = lc($text);
# split the text we're searching for into separate words. As a hack
# to allow quicksearch to work, if the field starts and ends with
# a double-quote, then we don't split it into words. We can't use
# Text::ParseWords here because it gets very confused by unbalanced
# quotes, which breaks searches like "don't try this" (because of the
# unbalanced single-quote in "don't").
my @words;
if ($lower_text =~ /^"/ and $lower_text =~ /"$/) {
$lower_text =~ s/^"//;
$lower_text =~ s/"$//;
@words = ($lower_text);
}
else {
@words = split(/\s+/, $lower_text);
}
# surround the words with wildcards and SQL quotes so we can use them
# in LIKE search clauses
@words = map($self->quote("\%$_\%"), @words);
# turn the words into a set of LIKE search clauses
@words = map("LOWER($column) LIKE $_", @words);
# search for occurrences of all specified words in the column
return join(" AND ", @words),
"CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END";
}
#####################################################################
# General Info Methods
#####################################################################
# XXX - Needs to be documented.
sub bz_server_version {
my ($self) = @_;
return $self->get_info(18); # SQL_DBMS_VER
}
sub bz_last_key {
my ($self, $table, $column) = @_;
return $self->last_insert_id(Bugzilla->localconfig->db_name, undef, $table,
$column);
}
sub bz_check_regexp {
my ($self, $pattern) = @_;
eval {
$self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1));
};
$@
&& ThrowUserError('illegal_regexp',
{value => $pattern, dberror => $self->errstr});
}
#####################################################################
# Database Setup
#####################################################################
sub bz_setup_database {
my ($self) = @_;
# If we haven't ever stored a serialized schema,
# set up the bz_schema table and store it.
$self->_bz_init_schema_storage();
# We don't use bz_table_list here, because that uses _bz_real_schema.
# We actually want the table list from the ABSTRACT_SCHEMA in
# Bugzilla::DB::Schema.
my @desired_tables = $self->_bz_schema->get_table_list();
my $bugs_exists = $self->bz_table_info('bugs');
if (!$bugs_exists) {
print install_string('db_table_setup'), "\n";
}
foreach my $table_name (@desired_tables) {
$self->bz_add_table($table_name, {silently => !$bugs_exists});
}
}
# This really just exists to get overridden in Bugzilla::DB::Mysql.
sub bz_enum_initial_values {
return ENUM_DEFAULTS;
}
sub bz_populate_enum_tables {
my ($self) = @_;
my $any_severities
= $self->selectrow_array('SELECT 1 FROM bug_severity ' . $self->sql_limit(1));
print install_string('db_enum_setup'), "\n " if !$any_severities;
my $enum_values = $self->bz_enum_initial_values();
while (my ($table, $values) = each %$enum_values) {
$self->_bz_populate_enum_table($table, $values);
}
print "\n" if !$any_severities;
}
sub bz_setup_foreign_keys {
my ($self) = @_;
# profiles_activity was the first table to get foreign keys,
# so if it doesn't have them, then we're setting up FKs
# for the first time, and should be quieter about it.
my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid');
my $any_fks = $activity_fk && $activity_fk->{created};
if (!$any_fks) {
print get_text('install_fk_setup'), "\n";
}
my @tables = $self->bz_table_list();
foreach my $table (@tables) {
my @columns = $self->bz_table_columns($table);
my %add_fks;
foreach my $column (@columns) {
# First we check for any FKs that have created => 0,
# in the _bz_real_schema. This also picks up FKs with
# created => 1, but bz_add_fks will ignore those.
my $fk = $self->bz_fk_info($table, $column);
# Then we check the abstract schema to see if there
# should be an FK on this column, but one wasn't set in the
# _bz_real_schema for some reason. We do this to handle
# various problems caused by upgrading from versions
# prior to 4.2, and also to handle problems caused
# by enabling an extension pre-4.2, disabling it for
# the 4.2 upgrade, and then re-enabling it later.
unless ($fk && $fk->{created}) {
my $standard_def = $self->_bz_schema->get_column_abstract($table, $column);
if (exists $standard_def->{REFERENCES}) {
$fk = dclone($standard_def->{REFERENCES});
}
}
$add_fks{$column} = $fk if $fk;
}
$self->bz_add_fks($table, \%add_fks, {silently => !$any_fks});
}
}
# This is used by contrib/bzdbcopy.pl, mostly.
sub bz_drop_foreign_keys {
my ($self) = @_;
my @tables = $self->bz_table_list();
foreach my $table (@tables) {
my @columns = $self->bz_table_columns($table);
foreach my $column (@columns) {
$self->bz_drop_fk($table, $column);
}
}
}
#####################################################################
# Schema Modification Methods
#####################################################################
sub bz_add_column {
my ($self, $table, $name, $new_def, $init_value) = @_;
# You can't add a NOT NULL column to a table with
# no DEFAULT statement, unless you have an init_value.
# SERIAL types are an exception, though, because they can
# auto-populate.
if ( $new_def->{NOTNULL}
&& !exists $new_def->{DEFAULT}
&& !defined $init_value
&& $new_def->{TYPE} !~ /SERIAL/)
{
ThrowCodeError('column_not_null_without_default', {name => "$table.$name"});
}
my $current_def = $self->bz_column_info($table, $name);
if (!$current_def) {
# REFERENCES need to happen later and not be created right away
my $trimmed_def = dclone($new_def);
delete $trimmed_def->{REFERENCES};
my @statements
= $self->_bz_real_schema->get_add_column_ddl($table, $name, $trimmed_def,
defined $init_value ? $self->quote($init_value) : undef);
print get_text('install_column_add', {column => $name, table => $table}) . "\n"
if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
foreach my $sql (@statements) {
$self->do($sql);
}
# To make things easier for callers, if they don't specify
# a REFERENCES item, we pull it from the _bz_schema if the
# column exists there and has a REFERENCES item.
# bz_setup_foreign_keys will then add this FK at the end of
# Install::DB.
my $col_abstract = $self->_bz_schema->get_column_abstract($table, $name);
if (exists $col_abstract->{REFERENCES}) {
my $new_fk = dclone($col_abstract->{REFERENCES});
$new_fk->{created} = 0;
$new_def->{REFERENCES} = $new_fk;
}
$self->_bz_real_schema->set_column($table, $name, $new_def);
$self->_bz_store_real_schema;
}
}
sub bz_add_fk {
my ($self, $table, $column, $def) = @_;
$self->bz_add_fks($table, {$column => $def});
}
sub bz_add_fks {
my ($self, $table, $column_fks, $options) = @_;
my %add_these;
foreach my $column (keys %$column_fks) {
my $current_fk = $self->bz_fk_info($table, $column);
next if ($current_fk and $current_fk->{created});
my $new_fk = $column_fks->{$column};
$self->_check_references($table, $column, $new_fk);
$add_these{$column} = $new_fk;
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) {
print get_text(
'install_fk_add', {table => $table, column => $column, fk => $new_fk}
),
"\n";
}
}
return if !scalar(keys %add_these);
my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these);
$self->do($_) foreach @sql;
foreach my $column (keys %add_these) {
my $fk_def = $add_these{$column};
$fk_def->{created} = 1;
$self->_bz_real_schema->set_fk($table, $column, $fk_def);
}
$self->_bz_store_real_schema();
}
sub bz_alter_column {
my ($self, $table, $name, $new_def, $set_nulls_to) = @_;
my $current_def = $self->bz_column_info($table, $name);
if (!$self->_bz_schema->columns_equal($current_def, $new_def)) {
# You can't change a column to be NOT NULL if you have no DEFAULT
# and no value for $set_nulls_to, if there are any NULL values
# in that column.
if ( $new_def->{NOTNULL}
&& !exists $new_def->{DEFAULT}
&& !defined $set_nulls_to)
{
# Check for NULLs
my $any_nulls
= $self->selectrow_array("SELECT 1 FROM $table WHERE $name IS NULL");
ThrowCodeError('column_not_null_no_default_alter', {name => "$table.$name"})
if ($any_nulls);
}
# Preserve foreign key definitions in the Schema object when altering
# types.
if (my $fk = $self->bz_fk_info($table, $name)) {
$new_def->{REFERENCES} = $fk;
}
$self->bz_alter_column_raw($table, $name, $new_def, $current_def,
$set_nulls_to);
$self->_bz_real_schema->set_column($table, $name, $new_def);
$self->_bz_store_real_schema;
}
}
# bz_alter_column_raw($table, $name, $new_def, $current_def)
#
# Description: A helper function for bz_alter_column.
# Alters a column in the database
# without updating any Schema object. Generally
# should only be called by bz_alter_column.
# Used when either: (1) You don't yet have a Schema
# object but you need to alter a column, for some reason.
# (2) You need to alter a column for some database-specific
# reason.
# Params: $table - The name of the table the column is on.
# $name - The name of the column you're changing.
# $new_def - The abstract definition that you are changing
# this column to.
# $current_def - (optional) The current definition of the
# column. Will be used in the output message,
# if given.
# $set_nulls_to - The same as the param of the same name
# from bz_alter_column.
# Returns: nothing
#
sub bz_alter_column_raw {
my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_;
my @statements
= $self->_bz_real_schema->get_alter_column_ddl($table, $name, $new_def,
defined $set_nulls_to ? $self->quote($set_nulls_to) : undef);
my $new_ddl = $self->_bz_schema->get_type_ddl($new_def);
print "Updating column $name in table $table ...\n";
if (defined $current_def) {
my $old_ddl = $self->_bz_schema->get_type_ddl($current_def);
print "Old: $old_ddl\n";
}
print "New: $new_ddl\n";
$self->do($_) foreach (@statements);
}
sub bz_alter_fk {
my ($self, $table, $column, $fk_def) = @_;
my $current_fk = $self->bz_fk_info($table, $column);
ThrowCodeError('column_alter_nonexistent_fk',
{table => $table, column => $column})
if !$current_fk;
$self->bz_drop_fk($table, $column);
$self->bz_add_fk($table, $column, $fk_def);
}
sub bz_add_index {
my ($self, $table, $name, $definition) = @_;
my $index_exists = $self->bz_index_info($table, $name);
if (!$index_exists) {
$self->bz_add_index_raw($table, $name, $definition);
$self->_bz_real_schema->set_index($table, $name, $definition);
$self->_bz_store_real_schema;
}
}
# bz_add_index_raw($table, $name, $silent)
#
# Description: A helper function for bz_add_index.
# Adds an index to the database
# without updating any Schema object. Generally
# should only be called by bz_add_index.
# Used when you don't yet have a Schema
# object but you need to add an index, for some reason.
# Params: $table - The name of the table the index is on.
# $name - The name of the index you're adding.
# $definition - The abstract index definition, in hashref
# or arrayref format.
# $silent - (optional) If specified and true, don't output
# any message about this change.
# Returns: nothing
#
sub bz_add_index_raw {
my ($self, $table, $name, $definition, $silent) = @_;
my @statements
= $self->_bz_schema->get_add_index_ddl($table, $name, $definition);
print "Adding new index '$name' to the $table table ...\n" unless $silent;
$self->do($_) foreach (@statements);
}
sub bz_add_table {
my ($self, $name, $options) = @_;
my $table_exists = $self->bz_table_info($name);
if (!$table_exists) {
$self->_bz_add_table_raw($name, $options);
my $table_def = dclone($self->_bz_schema->get_table_abstract($name));
my %fields = @{$table_def->{FIELDS}};
foreach my $col (keys %fields) {
# Foreign Key references have to be added by Install::DB after
# initial table creation, because column names have changed
# over history and it's impossible to keep track of that info
# in ABSTRACT_SCHEMA.
next unless exists $fields{$col}->{REFERENCES};
$fields{$col}->{REFERENCES}->{created} = $self->_bz_real_schema->FK_ON_CREATE;
}
$self->_bz_real_schema->add_table($name, $table_def);
$self->_bz_store_real_schema;
}
}
# _bz_add_table_raw($name) - Private
#
# Description: A helper function for bz_add_table.
# Creates a table in the database without
# updating any Schema object. Generally
# should only be called by bz_add_table and by
# _bz_init_schema_storage. Used when you don't
# yet have a Schema object but you need to
# add a table, for some reason.
# Params: $name - The name of the table you're creating.
# The definition for the table is pulled from
# _bz_schema.
# Returns: nothing
#
sub _bz_add_table_raw {
my ($self, $name, $options) = @_;
my @statements = $self->_bz_schema->get_table_ddl($name);
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) {
print install_string('db_table_new', {table => $name}), "\n";
}
$self->do($_) foreach (@statements);
}
sub _bz_add_field_table {
my ($self, $name, $schema_ref) = @_;
# We do nothing if the table already exists.
return if $self->bz_table_info($name);
# Copy this so that we're not modifying the passed reference.
# (This avoids modifying a constant in Bugzilla::DB::Schema.)
my %table_schema = %$schema_ref;
my %indexes = @{$table_schema{INDEXES}};
my %fixed_indexes;
foreach my $key (keys %indexes) {
$fixed_indexes{$name . "_" . $key} = $indexes{$key};
}
# INDEXES is supposed to be an arrayref, so we have to convert back.
my @indexes_array = %fixed_indexes;
$table_schema{INDEXES} = \@indexes_array;
# We add this to the abstract schema so that bz_add_table can find it.
$self->_bz_schema->add_table($name, \%table_schema);
$self->bz_add_table($name);
}
sub bz_add_field_tables {
my ($self, $field) = @_;
$self->_bz_add_field_table($field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA,
$field->type);
if ($field->type == FIELD_TYPE_MULTI_SELECT) {
my $ms_table = "bug_" . $field->name;
$self->_bz_add_field_table($ms_table,
$self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
$self->bz_add_fks(
$ms_table,
{
bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'},
value => {TABLE => $field->name, COLUMN => 'value'}
}
);
}
}
sub bz_drop_field_tables {
my ($self, $field) = @_;
if ($field->type == FIELD_TYPE_MULTI_SELECT) {
$self->bz_drop_table('bug_' . $field->name);
}
$self->bz_drop_table($field->name);
}
sub bz_drop_column {
my ($self, $table, $column) = @_;
my $current_def = $self->bz_column_info($table, $column);
if ($current_def) {
my @statements = $self->_bz_real_schema->get_drop_column_ddl($table, $column);
print get_text('install_column_drop', {table => $table, column => $column})
. "\n"
if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
foreach my $sql (@statements) {
# Because this is a deletion, we don't want to die hard if
# we fail because of some local customization. If something
# is already gone, that's fine with us!
eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
}
$self->_bz_real_schema->delete_column($table, $column);
$self->_bz_store_real_schema;
}
}
sub bz_drop_fk {
my ($self, $table, $column) = @_;
my $fk_def = $self->bz_fk_info($table, $column);
if ($fk_def and $fk_def->{created}) {
print get_text('install_fk_drop',
{table => $table, column => $column, fk => $fk_def})
. "\n"
if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
my @statements
= $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def);
foreach my $sql (@statements) {
# Because this is a deletion, we don't want to die hard if
# we fail because of some local customization. If something
# is already gone, that's fine with us!
eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@";
}
# Under normal circumstances, we don't permanently drop the fk--
# we want checksetup to re-create it again later. The only
# time that FKs get permanently dropped is if the column gets
# dropped.
$fk_def->{created} = 0;
$self->_bz_real_schema->set_fk($table, $column, $fk_def);
$self->_bz_store_real_schema;
}