Mysql.pm 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. # -*- Mode: perl; indent-tabs-mode: nil -*-
  2. #
  3. # The contents of this file are subject to the Mozilla Public
  4. # License Version 1.1 (the "License"); you may not use this file
  5. # except in compliance with the License. You may obtain a copy of
  6. # the License at http://www.mozilla.org/MPL/
  7. #
  8. # Software distributed under the License is distributed on an "AS
  9. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  10. # implied. See the License for the specific language governing
  11. # rights and limitations under the License.
  12. #
  13. # The Original Code is the Bugzilla Bug Tracking System.
  14. #
  15. # The Initial Developer of the Original Code is Netscape Communications
  16. # Corporation. Portions created by Netscape are
  17. # Copyright (C) 1998 Netscape Communications Corporation. All
  18. # Rights Reserved.
  19. #
  20. # Contributor(s): Dave Miller <davem00@aol.com>
  21. # Gayathri Swaminath <gayathrik00@aol.com>
  22. # Jeroen Ruigrok van der Werven <asmodai@wxs.nl>
  23. # Dave Lawrence <dkl@redhat.com>
  24. # Tomas Kopal <Tomas.Kopal@altap.cz>
  25. # Max Kanat-Alexander <mkanat@bugzilla.org>
  26. # Lance Larsh <lance.larsh@oracle.com>
  27. =head1 NAME
  28. Bugzilla::DB::Mysql - Bugzilla database compatibility layer for MySQL
  29. =head1 DESCRIPTION
  30. This module overrides methods of the Bugzilla::DB module with MySQL specific
  31. implementation. It is instantiated by the Bugzilla::DB module and should never
  32. be used directly.
  33. For interface details see L<Bugzilla::DB> and L<DBI>.
  34. =cut
  35. package Bugzilla::DB::Mysql;
  36. use strict;
  37. use Bugzilla::Constants;
  38. use Bugzilla::Install::Util qw(install_string);
  39. use Bugzilla::Util;
  40. use Bugzilla::Error;
  41. use Bugzilla::DB::Schema::Mysql;
  42. use List::Util qw(max);
  43. # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug.
  44. # In reality, you could have a LOT more comments than this, because
  45. # MAX_COMMENT_LENGTH is big.
  46. use constant MAX_COMMENTS => 50;
  47. # This module extends the DB interface via inheritance
  48. use base qw(Bugzilla::DB);
  49. sub new {
  50. my ($class, $user, $pass, $host, $dbname, $port, $sock) = @_;
  51. # construct the DSN from the parameters we got
  52. my $dsn = "DBI:mysql:host=$host;database=$dbname";
  53. $dsn .= ";port=$port" if $port;
  54. $dsn .= ";mysql_socket=$sock" if $sock;
  55. my $attrs = { mysql_enable_utf8 => Bugzilla->params->{'utf8'} };
  56. my $self = $class->db_new($dsn, $user, $pass, $attrs);
  57. # This makes sure that if the tables are encoded as UTF-8, we
  58. # return their data correctly.
  59. $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'};
  60. # all class local variables stored in DBI derived class needs to have
  61. # a prefix 'private_'. See DBI documentation.
  62. $self->{private_bz_tables_locked} = "";
  63. bless ($self, $class);
  64. # Bug 321645 - disable MySQL strict mode, if set
  65. my ($var, $sql_mode) = $self->selectrow_array(
  66. "SHOW VARIABLES LIKE 'sql\\_mode'");
  67. if ($sql_mode) {
  68. # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
  69. # causing bug 321645. TRADITIONAL sets these modes (among others) as
  70. # well, so it has to be stipped as well
  71. my $new_sql_mode =
  72. join(",", grep {$_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/}
  73. split(/,/, $sql_mode));
  74. if ($sql_mode ne $new_sql_mode) {
  75. $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
  76. }
  77. }
  78. # Allow large GROUP_CONCATs (largely for inserting comments
  79. # into bugs_fulltext).
  80. $self->do('SET SESSION group_concat_max_len = 128000000');
  81. return $self;
  82. }
  83. # when last_insert_id() is supported on MySQL by lowest DBI/DBD version
  84. # required by Bugzilla, this implementation can be removed.
  85. sub bz_last_key {
  86. my ($self) = @_;
  87. my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()');
  88. return $last_insert_id;
  89. }
  90. sub sql_group_concat {
  91. my ($self, $column, $separator) = @_;
  92. my $sep_sql;
  93. if ($separator) {
  94. $sep_sql = " SEPARATOR $separator";
  95. }
  96. return "GROUP_CONCAT($column$sep_sql)";
  97. }
  98. sub sql_regexp {
  99. my ($self, $expr, $pattern) = @_;
  100. return "$expr REGEXP $pattern";
  101. }
  102. sub sql_not_regexp {
  103. my ($self, $expr, $pattern) = @_;
  104. return "$expr NOT REGEXP $pattern";
  105. }
  106. sub sql_limit {
  107. my ($self, $limit, $offset) = @_;
  108. if (defined($offset)) {
  109. return "LIMIT $offset, $limit";
  110. } else {
  111. return "LIMIT $limit";
  112. }
  113. }
  114. sub sql_string_concat {
  115. my ($self, @params) = @_;
  116. return 'CONCAT(' . join(', ', @params) . ')';
  117. }
  118. sub sql_fulltext_search {
  119. my ($self, $column, $text) = @_;
  120. # Add the boolean mode modifier if the search string contains
  121. # boolean operators.
  122. my $mode = ($text =~ /[+\-<>()~*"]/ ? "IN BOOLEAN MODE" : "");
  123. # quote the text for use in the MATCH AGAINST expression
  124. $text = $self->quote($text);
  125. # untaint the text, since it's safe to use now that we've quoted it
  126. trick_taint($text);
  127. return "MATCH($column) AGAINST($text $mode)";
  128. }
  129. sub sql_istring {
  130. my ($self, $string) = @_;
  131. return $string;
  132. }
  133. sub sql_from_days {
  134. my ($self, $days) = @_;
  135. return "FROM_DAYS($days)";
  136. }
  137. sub sql_to_days {
  138. my ($self, $date) = @_;
  139. return "TO_DAYS($date)";
  140. }
  141. sub sql_date_format {
  142. my ($self, $date, $format) = @_;
  143. $format = "%Y.%m.%d %H:%i:%s" if !$format;
  144. return "DATE_FORMAT($date, " . $self->quote($format) . ")";
  145. }
  146. sub sql_interval {
  147. my ($self, $interval, $units) = @_;
  148. return "INTERVAL $interval $units";
  149. }
  150. sub sql_iposition {
  151. my ($self, $fragment, $text) = @_;
  152. return "INSTR($text, $fragment)";
  153. }
  154. sub sql_position {
  155. my ($self, $fragment, $text) = @_;
  156. return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))";
  157. }
  158. sub sql_group_by {
  159. my ($self, $needed_columns, $optional_columns) = @_;
  160. # MySQL allows you to specify the minimal subset of columns to get
  161. # a unique result. While it does allow specifying all columns as
  162. # ANSI SQL requires, according to MySQL documentation, the fewer
  163. # columns you specify, the faster the query runs.
  164. return "GROUP BY $needed_columns";
  165. }
  166. sub _bz_get_initial_schema {
  167. my ($self) = @_;
  168. return $self->_bz_build_schema_from_disk();
  169. }
  170. #####################################################################
  171. # Database Setup
  172. #####################################################################
  173. sub bz_setup_database {
  174. my ($self) = @_;
  175. # The "comments" field of the bugs_fulltext table could easily exceed
  176. # MySQL's default max_allowed_packet. Also, MySQL should never have
  177. # a max_allowed_packet smaller than our max_attachment_size. So, we
  178. # warn the user here if max_allowed_packet is too small.
  179. my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH;
  180. my (undef, $current_max_allowed) = $self->selectrow_array(
  181. q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
  182. # This parameter is not yet defined when the DB is being built for
  183. # the very first time. The code below still works properly, however,
  184. # because the default maxattachmentsize is smaller than $min_max_allowed.
  185. my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024;
  186. my $needed_max_allowed = max($min_max_allowed, $max_attachment);
  187. if ($current_max_allowed < $needed_max_allowed) {
  188. warn install_string('max_allowed_packet',
  189. { current => $current_max_allowed,
  190. needed => $needed_max_allowed }) . "\n";
  191. }
  192. # Make sure the installation has InnoDB turned on, or we're going to be
  193. # doing silly things like making foreign keys on MyISAM tables, which is
  194. # hard to fix later. We do this up here because none of the code below
  195. # works if InnoDB is off. (Particularly if we've already converted the
  196. # tables to InnoDB.)
  197. my ($innodb_on) = @{$self->selectcol_arrayref(
  198. q{SHOW VARIABLES LIKE '%have_innodb%'}, {Columns=>[2]})};
  199. if ($innodb_on ne 'YES') {
  200. print <<EOT;
  201. InnoDB is disabled in your MySQL installation.
  202. Bugzilla requires InnoDB to be enabled.
  203. Please enable it and then re-run checksetup.pl.
  204. EOT
  205. exit 3;
  206. }
  207. # Figure out if any existing tables are of type ISAM and convert them
  208. # to type MyISAM if so. ISAM tables are deprecated in MySQL 3.23,
  209. # which Bugzilla now requires, and they don't support more than 16
  210. # indexes per table, which Bugzilla needs.
  211. my $table_status = $self->selectall_arrayref("SHOW TABLE STATUS");
  212. my @isam_tables;
  213. foreach my $row (@$table_status) {
  214. my ($name, $type) = @$row;
  215. push(@isam_tables, $name) if $type eq "ISAM";
  216. }
  217. if(scalar(@isam_tables)) {
  218. print "One or more of the tables in your existing MySQL database are\n"
  219. . "of type ISAM. ISAM tables are deprecated in MySQL 3.23 and\n"
  220. . "don't support more than 16 indexes per table, which \n"
  221. . "Bugzilla needs.\n Converting your ISAM tables to type"
  222. . " MyISAM:\n\n";
  223. foreach my $table (@isam_tables) {
  224. print "Converting table $table... ";
  225. $self->do("ALTER TABLE $table TYPE = MYISAM");
  226. print "done.\n";
  227. }
  228. print "\nISAM->MyISAM table conversion done.\n\n";
  229. }
  230. my ($sd_index_deleted, $longdescs_index_deleted);
  231. my @tables = $self->bz_table_list_real();
  232. # We want to convert tables to InnoDB, but it's possible that they have
  233. # fulltext indexes on them, and conversion will fail unless we remove
  234. # the indexes.
  235. if (grep($_ eq 'bugs', @tables)) {
  236. if ($self->bz_index_info_real('bugs', 'short_desc')) {
  237. $self->bz_drop_index_raw('bugs', 'short_desc');
  238. }
  239. if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) {
  240. $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx');
  241. $sd_index_deleted = 1; # Used for later schema cleanup.
  242. }
  243. }
  244. if (grep($_ eq 'longdescs', @tables)) {
  245. if ($self->bz_index_info_real('longdescs', 'thetext')) {
  246. $self->bz_drop_index_raw('longdescs', 'thetext');
  247. }
  248. if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) {
  249. $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx');
  250. $longdescs_index_deleted = 1; # For later schema cleanup.
  251. }
  252. }
  253. # Upgrade tables from MyISAM to InnoDB
  254. my @myisam_tables;
  255. foreach my $row (@$table_status) {
  256. my ($name, $type) = @$row;
  257. if ($type =~ /^MYISAM$/i
  258. && !grep($_ eq $name, Bugzilla::DB::Schema::Mysql::MYISAM_TABLES))
  259. {
  260. push(@myisam_tables, $name) ;
  261. }
  262. }
  263. if (scalar @myisam_tables) {
  264. print "Bugzilla now uses the InnoDB storage engine in MySQL for",
  265. " most tables.\nConverting tables to InnoDB:\n";
  266. foreach my $table (@myisam_tables) {
  267. print "Converting table $table... ";
  268. $self->do("ALTER TABLE $table TYPE = InnoDB");
  269. print "done.\n";
  270. }
  271. }
  272. $self->_after_table_status(\@tables);
  273. # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did
  274. # not provide explicit names for the table indexes. This means
  275. # that our upgrades will not be reliable, because we look for the name
  276. # of the index, not what fields it is on, when doing upgrades.
  277. # (using the name is much better for cross-database compatibility
  278. # and general reliability). It's also very important that our
  279. # Schema object be consistent with what is on the disk.
  280. #
  281. # While we're at it, we also fix some inconsistent index naming
  282. # from the original checkin of Bugzilla::DB::Schema.
  283. # We check for the existence of a particular "short name" index that
  284. # has existed at least since Bugzilla 2.8, and probably earlier.
  285. # For fixing the inconsistent naming of Schema indexes,
  286. # we also check for one of those inconsistently-named indexes.
  287. if (grep($_ eq 'bugs', @tables)
  288. && ($self->bz_index_info_real('bugs', 'assigned_to')
  289. || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) )
  290. {
  291. # This is a check unrelated to the indexes, to see if people are
  292. # upgrading from 2.18 or below, but somehow have a bz_schema table
  293. # already. This only happens if they have done a mysqldump into
  294. # a database without doing a DROP DATABASE first.
  295. # We just do the check here since this check is a reliable way
  296. # of telling that we are upgrading from a version pre-2.20.
  297. if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) {
  298. die("\nYou are upgrading from a version before 2.20, but the"
  299. . " bz_schema\ntable already exists. This means that you"
  300. . " restored a mysqldump into\nthe Bugzilla database without"
  301. . " first dropping the already-existing\nBugzilla database,"
  302. . " at some point. Whenever you restore a Bugzilla\ndatabase"
  303. . " backup, you must always drop the entire database first.\n\n"
  304. . "Please drop your Bugzilla database and restore it from a"
  305. . " backup that\ndoes not contain the bz_schema table. If for"
  306. . " some reason you cannot\ndo this, you can connect to your"
  307. . " MySQL database and drop the bz_schema\ntable, as a last"
  308. . " resort.\n");
  309. }
  310. my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs");
  311. # We estimate one minute for each 3000 bugs, plus 3 minutes just
  312. # to handle basic MySQL stuff.
  313. my $rename_time = int($bug_count / 3000) + 3;
  314. # And 45 minutes for every 15,000 attachments, per some experiments.
  315. my ($attachment_count) =
  316. $self->selectrow_array("SELECT COUNT(*) FROM attachments");
  317. $rename_time += int(($attachment_count * 45) / 15000);
  318. # If we're going to take longer than 5 minutes, we let the user know
  319. # and allow them to abort.
  320. if ($rename_time > 5) {
  321. print "\nWe are about to rename old indexes.\n"
  322. . "The estimated time to complete renaming is "
  323. . "$rename_time minutes.\n"
  324. . "You cannot interrupt this action once it has begun.\n"
  325. . "If you would like to cancel, press Ctrl-C now..."
  326. . " (Waiting 45 seconds...)\n\n";
  327. # Wait 45 seconds for them to respond.
  328. sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE};
  329. }
  330. print "Renaming indexes...\n";
  331. # We can't be interrupted, because of how the "if"
  332. # works above.
  333. local $SIG{INT} = 'IGNORE';
  334. local $SIG{TERM} = 'IGNORE';
  335. local $SIG{PIPE} = 'IGNORE';
  336. # Certain indexes had names in Schema that did not easily conform
  337. # to a standard. We store those names here, so that they
  338. # can be properly renamed.
  339. # Also, sometimes an old mysqldump would incorrectly rename
  340. # unique indexes to "PRIMARY", so we address that here, also.
  341. my $bad_names = {
  342. # 'when' is a possible leftover from Bugzillas before 2.8
  343. bugs_activity => ['when', 'bugs_activity_bugid_idx',
  344. 'bugs_activity_bugwhen_idx'],
  345. cc => ['PRIMARY'],
  346. longdescs => ['longdescs_bugid_idx',
  347. 'longdescs_bugwhen_idx'],
  348. flags => ['flags_bidattid_idx'],
  349. flaginclusions => ['flaginclusions_tpcid_idx'],
  350. flagexclusions => ['flagexclusions_tpc_id_idx'],
  351. keywords => ['PRIMARY'],
  352. milestones => ['PRIMARY'],
  353. profiles_activity => ['profiles_activity_when_idx'],
  354. group_control_map => ['group_control_map_gid_idx', 'PRIMARY'],
  355. user_group_map => ['PRIMARY'],
  356. group_group_map => ['PRIMARY'],
  357. email_setting => ['PRIMARY'],
  358. bug_group_map => ['PRIMARY'],
  359. category_group_map => ['PRIMARY'],
  360. watch => ['PRIMARY'],
  361. namedqueries => ['PRIMARY'],
  362. series_data => ['PRIMARY'],
  363. # series_categories is dealt with below, not here.
  364. };
  365. # The series table is broken and needs to have one index
  366. # dropped before we begin the renaming, because it had a
  367. # useless index on it that would cause a naming conflict here.
  368. if (grep($_ eq 'series', @tables)) {
  369. my $dropname;
  370. # This is what the bad index was called before Schema.
  371. if ($self->bz_index_info_real('series', 'creator_2')) {
  372. $dropname = 'creator_2';
  373. }
  374. # This is what the bad index is called in Schema.
  375. elsif ($self->bz_index_info_real('series', 'series_creator_idx')) {
  376. $dropname = 'series_creator_idx';
  377. }
  378. $self->bz_drop_index_raw('series', $dropname) if $dropname;
  379. }
  380. # The email_setting table also had the same problem.
  381. if( grep($_ eq 'email_setting', @tables)
  382. && $self->bz_index_info_real('email_setting',
  383. 'email_settings_user_id_idx') )
  384. {
  385. $self->bz_drop_index_raw('email_setting',
  386. 'email_settings_user_id_idx');
  387. }
  388. # Go through all the tables.
  389. foreach my $table (@tables) {
  390. # Will contain the names of old indexes as keys, and the
  391. # definition of the new indexes as a value. The values
  392. # include an extra hash key, NAME, with the new name of
  393. # the index.
  394. my %rename_indexes;
  395. # And go through all the columns on each table.
  396. my @columns = $self->bz_table_columns_real($table);
  397. # We also want to fix the silly naming of unique indexes
  398. # that happened when we first checked-in Bugzilla::DB::Schema.
  399. if ($table eq 'series_categories') {
  400. # The series_categories index had a nonstandard name.
  401. push(@columns, 'series_cats_unique_idx');
  402. }
  403. elsif ($table eq 'email_setting') {
  404. # The email_setting table had a similar problem.
  405. push(@columns, 'email_settings_unique_idx');
  406. }
  407. else {
  408. push(@columns, "${table}_unique_idx");
  409. }
  410. # And this is how we fix the other inconsistent Schema naming.
  411. push(@columns, @{$bad_names->{$table}})
  412. if (exists $bad_names->{$table});
  413. foreach my $column (@columns) {
  414. # If we have an index named after this column, it's an
  415. # old-style-name index.
  416. if (my $index = $self->bz_index_info_real($table, $column)) {
  417. # Fix the name to fit in with the new naming scheme.
  418. $index->{NAME} = $table . "_" .
  419. $index->{FIELDS}->[0] . "_idx";
  420. print "Renaming index $column to "
  421. . $index->{NAME} . "...\n";
  422. $rename_indexes{$column} = $index;
  423. } # if
  424. } # foreach column
  425. my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl(
  426. $table, %rename_indexes);
  427. $self->do($_) foreach (@rename_sql);
  428. } # foreach table
  429. } # if old-name indexes
  430. # If there are no tables, but the DB isn't utf8 and it should be,
  431. # then we should alter the database to be utf8. We know it should be
  432. # if the utf8 parameter is true or there are no params at all.
  433. # This kind of situation happens when people create the database
  434. # themselves, and if we don't do this they will get the big
  435. # scary WARNING statement about conversion to UTF8.
  436. if ( !$self->bz_db_is_utf8 && !@tables
  437. && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params}) )
  438. {
  439. $self->_alter_db_charset_to_utf8();
  440. }
  441. # And now we create the tables and the Schema object.
  442. $self->SUPER::bz_setup_database();
  443. if ($sd_index_deleted) {
  444. $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx');
  445. $self->_bz_store_real_schema;
  446. }
  447. if ($longdescs_index_deleted) {
  448. $self->_bz_real_schema->delete_index('longdescs',
  449. 'longdescs_thetext_idx');
  450. $self->_bz_store_real_schema;
  451. }
  452. # The old timestamp fields need to be adjusted here instead of in
  453. # checksetup. Otherwise the UPDATE statements inside of bz_add_column
  454. # will cause accidental timestamp updates.
  455. # The code that does this was moved here from checksetup.
  456. # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578
  457. # attachments creation time needs to be a datetime, not a timestamp
  458. my $attach_creation =
  459. $self->bz_column_info("attachments", "creation_ts");
  460. if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) {
  461. print "Fixing creation time on attachments...\n";
  462. my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments");
  463. $sth->execute();
  464. my ($attach_count) = $sth->fetchrow_array();
  465. if ($attach_count > 1000) {
  466. print "This may take a while...\n";
  467. }
  468. my $i = 0;
  469. # This isn't just as simple as changing the field type, because
  470. # the creation_ts was previously updated when an attachment was made
  471. # obsolete from the attachment creation screen. So we have to go
  472. # and recreate these times from the comments..
  473. $sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " .
  474. "FROM attachments");
  475. $sth->execute();
  476. # Restrict this as much as possible in order to avoid false
  477. # positives, and keep the db search time down
  478. my $sth2 = $self->prepare("SELECT bug_when FROM longdescs
  479. WHERE bug_id=? AND who=?
  480. AND thetext LIKE ?
  481. ORDER BY bug_when " . $self->sql_limit(1));
  482. while (my ($bug_id, $attach_id, $submitter_id)
  483. = $sth->fetchrow_array())
  484. {
  485. $sth2->execute($bug_id, $submitter_id,
  486. "Created an attachment (id=$attach_id)%");
  487. my ($when) = $sth2->fetchrow_array();
  488. if ($when) {
  489. $self->do("UPDATE attachments " .
  490. "SET creation_ts='$when' " .
  491. "WHERE attach_id=$attach_id");
  492. } else {
  493. print "Warning - could not determine correct creation"
  494. . " time for attachment $attach_id on bug $bug_id\n";
  495. }
  496. ++$i;
  497. print "Converted $i of $attach_count attachments\n" if !($i % 1000);
  498. }
  499. print "Done - converted $i attachments\n";
  500. $self->bz_alter_column("attachments", "creation_ts",
  501. {TYPE => 'DATETIME', NOTNULL => 1});
  502. }
  503. # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303
  504. # Change logincookies.lastused type from timestamp to datetime
  505. my $login_lastused = $self->bz_column_info("logincookies", "lastused");
  506. if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) {
  507. $self->bz_alter_column('logincookies', 'lastused',
  508. { TYPE => 'DATETIME', NOTNULL => 1});
  509. }
  510. # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315
  511. # Change bugs.delta_ts type from timestamp to datetime
  512. my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts");
  513. if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) {
  514. $self->bz_alter_column('bugs', 'delta_ts',
  515. {TYPE => 'DATETIME', NOTNULL => 1});
  516. }
  517. # 2005-09-24 - bugreport@peshkin.net, bug 307602
  518. # Make sure that default 4G table limit is overridden
  519. my $row = $self->selectrow_hashref("SHOW TABLE STATUS LIKE 'attach_data'");
  520. if ($$row{'Create_options'} !~ /MAX_ROWS/i) {
  521. print "Converting attach_data maximum size to 100G...\n";
  522. $self->do("ALTER TABLE attach_data
  523. AVG_ROW_LENGTH=1000000,
  524. MAX_ROWS=100000");
  525. }
  526. # Convert the database to UTF-8 if the utf8 parameter is on.
  527. # We check if any table isn't utf8, because lots of crazy
  528. # partial-conversion situations can happen, and this handles anything
  529. # that could come up (including having the DB charset be utf8 but not
  530. # the table charsets.
  531. my $utf_table_status =
  532. $self->selectall_arrayref("SHOW TABLE STATUS", {Slice=>{}});
  533. $self->_after_table_status([map($_->{Name}, @$utf_table_status)]);
  534. my @non_utf8_tables = grep($_->{Collation} !~ /^utf8/, @$utf_table_status);
  535. if (Bugzilla->params->{'utf8'} && scalar @non_utf8_tables) {
  536. print <<EOT;
  537. WARNING: We are about to convert your table storage format to UTF8. This
  538. allows Bugzilla to correctly store and sort international characters.
  539. However, if you have any non-UTF-8 data in your database,
  540. it ***WILL BE DELETED*** by this process. So, before
  541. you continue with checksetup.pl, if you have any non-UTF-8
  542. data (or even if you're not sure) you should press Ctrl-C now
  543. to interrupt checksetup.pl, and run contrib/recode.pl to make all
  544. the data in your database into UTF-8. You should also back up your
  545. database before continuing. This will affect every single table
  546. in the database, even non-Bugzilla tables.
  547. If you ever used a version of Bugzilla before 2.22, we STRONGLY
  548. recommend that you stop checksetup.pl NOW and run contrib/recode.pl.
  549. EOT
  550. if (!Bugzilla->installation_answers->{NO_PAUSE}) {
  551. if (Bugzilla->installation_mode ==
  552. INSTALLATION_MODE_NON_INTERACTIVE)
  553. {
  554. print <<EOT;
  555. Re-run checksetup.pl in interactive mode (without an 'answers' file)
  556. to continue.
  557. EOT
  558. exit;
  559. }
  560. else {
  561. print " Press Enter to continue or Ctrl-C to exit...";
  562. getc;
  563. }
  564. }
  565. print "Converting table storage format to UTF-8. This may take a",
  566. " while.\n";
  567. foreach my $table ($self->bz_table_list_real) {
  568. my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
  569. $info_sth->execute();
  570. while (my $column = $info_sth->fetchrow_hashref) {
  571. # Our conversion code doesn't work on enum fields, but they
  572. # all go away later in checksetup anyway.
  573. next if $column->{Type} =~ /enum/i;
  574. # If this particular column isn't stored in utf-8
  575. if ($column->{Collation}
  576. && $column->{Collation} ne 'NULL'
  577. && $column->{Collation} !~ /utf8/)
  578. {
  579. my $name = $column->{Field};
  580. # The code below doesn't work on a field with a FULLTEXT
  581. # index. So we drop it, which we'd do later anyway.
  582. if ($table eq 'longdescs' && $name eq 'thetext') {
  583. $self->bz_drop_index('longdescs',
  584. 'longdescs_thetext_idx');
  585. }
  586. if ($table eq 'bugs' && $name eq 'short_desc') {
  587. $self->bz_drop_index('bugs', 'bugs_short_desc_idx');
  588. }
  589. my %ft_indexes;
  590. if ($table eq 'bugs_fulltext') {
  591. %ft_indexes = $self->_bz_real_schema->get_indexes_on_column_abstract(
  592. 'bugs_fulltext', $name);
  593. foreach my $index (keys %ft_indexes) {
  594. $self->bz_drop_index('bugs_fulltext', $index);
  595. }
  596. }
  597. print "Converting $table.$name to be stored as UTF-8...\n";
  598. my $col_info =
  599. $self->bz_column_info_real($table, $name);
  600. # CHANGE COLUMN doesn't take PRIMARY KEY
  601. delete $col_info->{PRIMARYKEY};
  602. my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
  603. # We don't want MySQL to actually try to *convert*
  604. # from our current charset to UTF-8, we just want to
  605. # transfer the bytes directly. This is how we do that.
  606. # The CHARACTER SET part of the definition has to come
  607. # right after the type, which will always come first.
  608. my ($binary, $utf8) = ($sql_def, $sql_def);
  609. my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
  610. $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
  611. $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/;
  612. $self->do("ALTER TABLE $table CHANGE COLUMN $name $name
  613. $binary");
  614. $self->do("ALTER TABLE $table CHANGE COLUMN $name $name
  615. $utf8");
  616. if ($table eq 'bugs_fulltext') {
  617. foreach my $index (keys %ft_indexes) {
  618. $self->bz_add_index('bugs_fulltext', $index,
  619. $ft_indexes{$index});
  620. }
  621. }
  622. }
  623. }
  624. $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
  625. } # foreach my $table (@tables)
  626. }
  627. # Sometimes you can have a situation where all the tables are utf8,
  628. # but the database isn't. (This tends to happen when you've done
  629. # a mysqldump.) So we have this change outside of the above block,
  630. # so that it just happens silently if no actual *table* conversion
  631. # needs to happen.
  632. if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) {
  633. $self->_alter_db_charset_to_utf8();
  634. }
  635. }
  636. # There is a bug in MySQL 4.1.0 - 4.1.15 that makes certain SELECT
  637. # statements fail after a SHOW TABLE STATUS:
  638. # http://bugs.mysql.com/bug.php?id=13535
  639. # This is a workaround, a dummy SELECT to reset the LAST_INSERT_ID.
  640. sub _after_table_status {
  641. my ($self, $tables) = @_;
  642. if (grep($_ eq 'bugs', @$tables)
  643. && $self->bz_column_info_real("bugs", "bug_id"))
  644. {
  645. $self->do('SELECT 1 FROM bugs WHERE bug_id IS NULL');
  646. }
  647. }
  648. sub _alter_db_charset_to_utf8 {
  649. my $self = shift;
  650. my $db_name = Bugzilla->localconfig->{db_name};
  651. $self->do("ALTER DATABASE $db_name CHARACTER SET utf8");
  652. }
  653. sub bz_db_is_utf8 {
  654. my $self = shift;
  655. my $db_collation = $self->selectrow_arrayref(
  656. "SHOW VARIABLES LIKE 'character_set_database'");
  657. # First column holds the variable name, second column holds the value.
  658. return $db_collation->[1] =~ /utf8/ ? 1 : 0;
  659. }
  660. sub bz_enum_initial_values {
  661. my ($self) = @_;
  662. my %enum_values = %{$self->ENUM_DEFAULTS};
  663. # Get a complete description of the 'bugs' table; with DBD::MySQL
  664. # there isn't a column-by-column way of doing this. Could use
  665. # $dbh->column_info, but it would go slower and we would have to
  666. # use the undocumented mysql_type_name accessor to get the type
  667. # of each row.
  668. my $sth = $self->prepare("DESCRIBE bugs");
  669. $sth->execute();
  670. # Look for the particular columns we are interested in.
  671. while (my ($thiscol, $thistype) = $sth->fetchrow_array()) {
  672. if (defined $enum_values{$thiscol}) {
  673. # this is a column of interest.
  674. my @value_list;
  675. if ($thistype and ($thistype =~ /^enum\(/)) {
  676. # it has an enum type; get the set of values.
  677. while ($thistype =~ /'([^']*)'(.*)/) {
  678. push(@value_list, $1);
  679. $thistype = $2;
  680. }
  681. }
  682. if (@value_list) {
  683. # record the enum values found.
  684. $enum_values{$thiscol} = \@value_list;
  685. }
  686. }
  687. }
  688. return \%enum_values;
  689. }
  690. #####################################################################
  691. # MySQL-specific Database-Reading Methods
  692. #####################################################################
  693. =begin private
  694. =head1 MYSQL-SPECIFIC DATABASE-READING METHODS
  695. These methods read information about the database from the disk,
  696. instead of from a Schema object. They are only reliable for MySQL
  697. (see bug 285111 for the reasons why not all DBs use/have functions
  698. like this), but that's OK because we only need them for
  699. backwards-compatibility anyway, for versions of Bugzilla before 2.20.
  700. =over 4
  701. =item C<bz_column_info_real($table, $column)>
  702. Description: Returns an abstract column definition for a column
  703. as it actually exists on disk in the database.
  704. Params: $table - The name of the table the column is on.
  705. $column - The name of the column you want info about.
  706. Returns: An abstract column definition.
  707. If the column does not exist, returns undef.
  708. =cut
  709. sub bz_column_info_real {
  710. my ($self, $table, $column) = @_;
  711. # DBD::mysql does not support selecting a specific column,
  712. # so we have to get all the columns on the table and find
  713. # the one we want.
  714. my $info_sth = $self->column_info(undef, undef, $table, '%');
  715. # Don't use fetchall_hashref as there's a Win32 DBI bug (292821)
  716. my $col_data;
  717. while ($col_data = $info_sth->fetchrow_hashref) {
  718. last if $col_data->{'COLUMN_NAME'} eq $column;
  719. }
  720. if (!defined $col_data) {
  721. return undef;
  722. }
  723. return $self->_bz_schema->column_info_to_column($col_data);
  724. }
  725. =item C<bz_index_info_real($table, $index)>
  726. Description: Returns information about an index on a table in the database.
  727. Params: $table = name of table containing the index
  728. $index = name of an index
  729. Returns: An abstract index definition, always in hashref format.
  730. If the index does not exist, the function returns undef.
  731. =cut
  732. sub bz_index_info_real {
  733. my ($self, $table, $index) = @_;
  734. my $sth = $self->prepare("SHOW INDEX FROM $table");
  735. $sth->execute;
  736. my @fields;
  737. my $index_type;
  738. # $raw_def will be an arrayref containing the following information:
  739. # 0 = name of the table that the index is on
  740. # 1 = 0 if unique, 1 if not unique
  741. # 2 = name of the index
  742. # 3 = seq_in_index (The order of the current field in the index).
  743. # 4 = Name of ONE column that the index is on
  744. # 5 = 'Collation' of the index. Usually 'A'.
  745. # 6 = Cardinality. Either a number or undef.
  746. # 7 = sub_part. Usually undef. Sometimes 1.
  747. # 8 = "packed". Usually undef.
  748. # 9 = Null. Sometimes undef, sometimes 'YES'.
  749. # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT'
  750. # 11 = 'Comment.' Usually undef.
  751. while (my $raw_def = $sth->fetchrow_arrayref) {
  752. if ($raw_def->[2] eq $index) {
  753. push(@fields, $raw_def->[4]);
  754. # No index can be both UNIQUE and FULLTEXT, that's why
  755. # this is written this way.
  756. $index_type = $raw_def->[1] ? '' : 'UNIQUE';
  757. $index_type = $raw_def->[10] eq 'FULLTEXT'
  758. ? 'FULLTEXT' : $index_type;
  759. }
  760. }
  761. my $retval;
  762. if (scalar(@fields)) {
  763. $retval = {FIELDS => \@fields, TYPE => $index_type};
  764. }
  765. return $retval;
  766. }
  767. =item C<bz_index_list_real($table)>
  768. Description: Returns a list of index names on a table in
  769. the database, as it actually exists on disk.
  770. Params: $table - The name of the table you want info about.
  771. Returns: An array of index names.
  772. =cut
  773. sub bz_index_list_real {
  774. my ($self, $table) = @_;
  775. my $sth = $self->prepare("SHOW INDEX FROM $table");
  776. # Column 3 of a SHOW INDEX statement contains the name of the index.
  777. return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) };
  778. }
  779. #####################################################################
  780. # MySQL-Specific "Schema Builder"
  781. #####################################################################
  782. =back
  783. =head1 MYSQL-SPECIFIC "SCHEMA BUILDER"
  784. MySQL needs to be able to read in a legacy database (from before
  785. Schema existed) and create a Schema object out of it. That's what
  786. this code does.
  787. =end private
  788. =cut
  789. # This sub itself is actually written generically, but the subroutines
  790. # that it depends on are database-specific. In particular, the
  791. # bz_column_info_real function would be very difficult to create
  792. # properly for any other DB besides MySQL.
  793. sub _bz_build_schema_from_disk {
  794. my ($self) = @_;
  795. print "Building Schema object from database...\n";
  796. my $schema = $self->_bz_schema->get_empty_schema();
  797. my @tables = $self->bz_table_list_real();
  798. foreach my $table (@tables) {
  799. $schema->add_table($table);
  800. my @columns = $self->bz_table_columns_real($table);
  801. foreach my $column (@columns) {
  802. my $type_info = $self->bz_column_info_real($table, $column);
  803. $schema->set_column($table, $column, $type_info);
  804. }
  805. my @indexes = $self->bz_index_list_real($table);
  806. foreach my $index (@indexes) {
  807. unless ($index eq 'PRIMARY') {
  808. my $index_info = $self->bz_index_info_real($table, $index);
  809. ($index_info = $index_info->{FIELDS})
  810. if (!$index_info->{TYPE});
  811. $schema->set_index($table, $index, $index_info);
  812. }
  813. }
  814. }
  815. return $schema;
  816. }
  817. 1;