File Coverage

blib/lib/DBIx/OnlineDDL.pm
Criterion Covered Total %
statement 364 493 73.8
branch 43 136 31.6
condition 32 120 26.6
subroutine 43 56 76.7
pod 11 12 91.6
total 493 817 60.3


line stmt bran cond sub pod time code
1             package DBIx::OnlineDDL;
2              
3             our $AUTHORITY = 'cpan:GSG';
4             # ABSTRACT: Run DDL on online databases safely
5 2     2   1129539 use version;
  2         22  
  2         19  
6             our $VERSION = 'v0.930.1'; # VERSION
7              
8 2     2   190 use v5.10;
  2         8  
9 2     2   12 use Moo;
  2         5  
  2         12  
10 2     2   726 use MooX::StrictConstructor;
  2         11  
  2         40  
11              
12 2     2   1955 use Types::Standard qw( Str Bool HashRef CodeRef InstanceOf Dict Optional );
  2         6  
  2         19  
13 2     2   3048 use Types::Common::Numeric qw( PositiveNum PositiveInt );
  2         4  
  2         18  
14              
15 2     2   1882 use Class::Load;
  2         20704  
  2         107  
16 2     2   875 use DBI::Const::GetInfoType;
  2         15356  
  2         295  
17 2     2   17 use DBIx::BatchChunker 0.92; # with stmt attrs
  2         64  
  2         45  
18 2     2   1028 use Eval::Reversible;
  2         54984  
  2         114  
19 2     2   16 use List::Util 1.44 (qw( uniq any all )); # 1.44 has uniq
  2         42  
  2         129  
20 2     2   15 use Sub::Util qw( subname set_subname );
  2         4  
  2         118  
21 2     2   12 use Term::ProgressBar 2.14; # with silent option
  2         34  
  2         70  
22              
23             # Don't export the above, but don't conflict with StrictConstructor, either
24 2     2   13 use namespace::clean -except => [qw< new meta >];
  2         7  
  2         19  
25              
26             my $DEFAULT_MAX_ATTEMPTS = 20;
27              
28             #pod =encoding utf8
29             #pod
30             #pod =head1 SYNOPSIS
31             #pod
32             #pod use DBIx::OnlineDDL;
33             #pod use DBIx::BatchChunker;
34             #pod
35             #pod DBIx::OnlineDDL->construct_and_execute(
36             #pod rsrc => $dbic_schema->source('Account'),
37             #pod ### OR ###
38             #pod dbi_connector => $dbix_connector_retry_object,
39             #pod table_name => 'accounts',
40             #pod
41             #pod coderef_hooks => {
42             #pod # This is the phase where the DDL is actually run
43             #pod before_triggers => \&drop_foobar,
44             #pod
45             #pod # Run other operations right before the swap
46             #pod before_swap => \&delete_deprecated_accounts,
47             #pod },
48             #pod
49             #pod process_name => 'Dropping foobar from accounts',
50             #pod
51             #pod copy_opts => {
52             #pod chunk_size => 5000,
53             #pod debug => 1,
54             #pod },
55             #pod );
56             #pod
57             #pod sub drop_foobar {
58             #pod my $oddl = shift;
59             #pod my $name = $oddl->new_table_name;
60             #pod my $qname = $oddl->dbh->quote_identifier($name);
61             #pod
62             #pod # Drop the 'foobar' column, since it is no longer used
63             #pod $oddl->dbh_runner_do("ALTER TABLE $qname DROP COLUMN foobar");
64             #pod }
65             #pod
66             #pod sub delete_deprecated_accounts {
67             #pod my $oddl = shift;
68             #pod my $name = $oddl->new_table_name;
69             #pod my $dbh = $oddl->dbh; # only use for quoting!
70             #pod
71             #pod my $qname = $dbh->quote_identifier($name);
72             #pod
73             #pod DBIx::BatchChunker->construct_and_execute(
74             #pod chunk_size => 5000,
75             #pod
76             #pod debug => 1,
77             #pod
78             #pod process_name => 'Deleting deprecated accounts',
79             #pod process_past_max => 1,
80             #pod
81             #pod dbic_storage => $oddl->rsrc->storage,
82             #pod min_stmt => "SELECT MIN(account_id) FROM $qname",
83             #pod max_stmt => "SELECT MAX(account_id) FROM $qname",
84             #pod stmt => join("\n",
85             #pod "DELETE FROM $qname",
86             #pod "WHERE",
87             #pod " account_type = ".$dbh->quote('deprecated')." AND",
88             #pod " account_id BETWEEN ? AND ?",
89             #pod ),
90             #pod );
91             #pod }
92             #pod
93             #pod =head1 DESCRIPTION
94             #pod
95             #pod This is a database utility class for running DDL operations (like C) safely
96             #pod on large tables. It has a similar scope as L, but is designed for
97             #pod DDL, rather than DML. It also has a similar function to other utilities like
98             #pod L or
99             #pod L, but actually works properly with foreign
100             #pod keys, and is written as a Perl module to hook directly into a DBI handle.
101             #pod
102             #pod Like most online schema change tools, this works by creating a new shell table that looks
103             #pod just like the old table, running the DDL changes (through the L hook),
104             #pod copying data to the new table, and swapping the tables. Triggers are created to keep the
105             #pod data in sync. See L for more information.
106             #pod
107             #pod The full operation is protected with an L via L.
108             #pod If any step in the process fails, the undo stack is run to return the DB back to normal.
109             #pod
110             #pod This module uses as many of the DBI info methods as possible, along with ANSI SQL in most
111             #pod places, to be compatible with multiple RDBMS. So far, it will work with MySQL or SQLite,
112             #pod but can be expanded to include more systems with a relatively small amount of code
113             #pod changes. (See L for details.)
114             #pod
115             #pod B You should not rely on this class to magically fix any and all locking
116             #pod problems the DB might experience just because it's being used. Thorough testing and
117             #pod best practices are still required.
118             #pod
119             #pod =head2 When you shouldn't use this module
120             #pod
121             #pod =head3 Online DDL is already available in the RDBMS
122             #pod
123             #pod If you're running MySQL 5.6+ without clustering, just use C for every DDL
124             #pod statement. It is seriously simple and guarantees that the table changes you make are not
125             #pod going to lock the table, or it will fail right away to tell you it's an incompatible
126             #pod change.
127             #pod
128             #pod If you're running something like Galera clusters, this typically wouldn't be an option,
129             #pod as it would lock up the clusters while the C statement is running, despite
130             #pod the C statement. (Galera clusters were the prime motivation for writing this
131             #pod module.)
132             #pod
133             #pod Other RDBMSs may have support for online DDL as well. Check the documentation first. If
134             #pod they don't, patches for this tool are welcome!
135             #pod
136             #pod =head3 The operation is small
137             #pod
138             #pod Does your DDL only take 2 seconds? Just do it! Don't bother with trying to swap tables
139             #pod around, wasting time with full table copies, etc. It's not worth the time spent or risk.
140             #pod
141             #pod =head3 When you actually want to run DML, not DDL
142             #pod
143             #pod L is more appropriate for running DML operations (like C,
144             #pod C, C). If you need to do both, you can use the L hook
145             #pod for DDL, and the L hook for DML. Or just run DBIx::BatchChunker after the
146             #pod OnlineDDL process is complete.
147             #pod
148             #pod =head3 Other online schema change tools fit your needs
149             #pod
150             #pod Don't have foreign key constraints and C is already working for you? Great!
151             #pod Keep using it.
152             #pod
153             #pod =head1 ATTRIBUTES
154             #pod
155             #pod =head2 DBIC Attributes
156             #pod
157             #pod =head3 rsrc
158             #pod
159             #pod A L. This will be the source used for all operations, DDL or
160             #pod otherwise. Optional, but recommended for DBIC users.
161             #pod
162             #pod The DBIC storage handler's C will be tweaked to ensure sane defaults and
163             #pod proper post-connection details.
164             #pod
165             #pod =cut
166              
167             has rsrc => (
168             is => 'ro',
169             isa => InstanceOf['DBIx::Class::ResultSource'],
170             required => 0,
171             );
172              
173             #pod =head3 dbic_retry_opts
174             #pod
175             #pod A hashref of DBIC retry options. These options control how retry protection works within
176             #pod DBIC. Right now, this is just limited to C, which controls the number of
177             #pod times to retry. The default C is 20.
178             #pod
179             #pod =cut
180              
181             has dbic_retry_opts => (
182             is => 'ro',
183             isa => HashRef,
184             required => 0,
185             default => sub { {} },
186             );
187              
188             #pod =head2 DBI Attributes
189             #pod
190             #pod =head3 dbi_connector
191             #pod
192             #pod A L object. Instead of L statement handles, this is the
193             #pod recommended non-DBIC way for OnlineDDL (and BatchChunker) to interface with the DBI, as
194             #pod it handles retries on failures. The connection mode used is whatever default is set
195             #pod within the object.
196             #pod
197             #pod Required, except for DBIC users, who should be setting L above. It is also
198             #pod assumed that the correct database is already active.
199             #pod
200             #pod The object will be tweaked to ensure sane defaults, proper post-connection details, a
201             #pod custom C, and set a default C of 20, if not already set.
202             #pod
203             #pod =cut
204              
205             has dbi_connector => (
206             is => 'ro',
207             isa => InstanceOf['DBIx::Connector::Retry'],
208             required => 0,
209             );
210              
211             #pod =head3 table_name
212             #pod
213             #pod The table name to be copied and eventually replaced. Required unless L is
214             #pod specified.
215             #pod
216             #pod =cut
217              
218             has table_name => (
219             is => 'ro',
220             isa => Str,
221             required => 1,
222             lazy => 1,
223             default => sub {
224             my $rsrc = shift->rsrc // return;
225             $rsrc->from;
226             },
227             );
228              
229             #pod =head3 new_table_name
230             #pod
231             #pod The new table name to be created, copied to, and eventually used as the final table.
232             #pod Optional.
233             #pod
234             #pod If not defined, a name will be created automatically. This might be the better route,
235             #pod since the default builder will search for an unused name in the DB right before OnlineDDL
236             #pod needs it.
237             #pod
238             #pod =cut
239              
240             has new_table_name => (
241             is => 'ro',
242             isa => Str,
243             required => 0,
244             lazy => 1,
245             builder => 1,
246             );
247              
248             sub _build_new_table_name {
249 27     27   11195 my $self = shift;
250 27         129 my $dbh = $self->dbh;
251 27         942021 my $vars = $self->_vars;
252              
253 27         268 my $catalog = $vars->{catalog};
254 27         78 my $schema = $vars->{schema};
255 27         560 my $orig_table_name = $self->table_name;
256              
257 27   50     516 my $escape = $dbh->get_info( $GetInfoType{SQL_SEARCH_PATTERN_ESCAPE} ) // '\\';
258              
259             return $self->_find_new_identifier(
260             "_${orig_table_name}_new" => set_subname('_new_table_name_finder', sub {
261 27     27   74 $dbh = shift;
262 27         1032 my $like_expr = shift;
263 27         484 $like_expr =~ s/([_%])/$escape$1/g;
264              
265 27         275 $dbh->table_info($catalog, $schema, $like_expr)->fetchrow_array;
266 27         925 }),
267             'SQL_MAXIMUM_TABLE_NAME_LENGTH',
268             );
269             }
270              
271             #pod =head2 Progress Bar Attributes
272             #pod
273             #pod =head3 progress_bar
274             #pod
275             #pod The progress bar used for most of the process. A different one is used for the actual
276             #pod table copy with L, since that step takes longer.
277             #pod
278             #pod Optional. If the progress bar isn't specified, a default one will be created. If the
279             #pod terminal isn't interactive, the default L will be set to C to
280             #pod naturally skip the output.
281             #pod
282             #pod =cut
283              
284             has progress_bar => (
285             is => 'rw',
286             isa => InstanceOf['Term::ProgressBar'],
287             );
288              
289             sub _progress_bar_setup {
290 27     27   73 my $self = shift;
291 27         517 my $vars = $self->_vars;
292              
293 27         211 my $steps = 6 + scalar keys %{ $self->coderef_hooks };
  27         140  
294              
295 27   33     539 my $progress = $self->progress_bar || Term::ProgressBar->new({
296             name => $self->progress_name,
297             count => $steps,
298             ETA => 'linear',
299             silent => !(-t *STDERR && -t *STDIN), # STDERR is what {fh} is set to use
300             });
301              
302 27         61640 $vars->{progress_bar} = $progress;
303             }
304              
305             #pod =head3 progress_name
306             #pod
307             #pod A string used to assist in creating a progress bar. Ignored if L is
308             #pod already specified.
309             #pod
310             #pod This is the preferred way of customizing the progress bar without having to create one
311             #pod from scratch.
312             #pod
313             #pod =cut
314              
315             has progress_name => (
316             is => 'rw',
317             isa => Str,
318             required => 0,
319             lazy => 1,
320             default => sub {
321             my $table_name = shift->table_name;
322             'Altering'.($table_name ? " $table_name" : '');
323             },
324             );
325              
326             #pod =head2 Other Attributes
327             #pod
328             #pod =head3 coderef_hooks
329             #pod
330             #pod A hashref of coderefs. Each of these are used in different steps in the process. All
331             #pod of these are optional, but it is B that C is
332             #pod specified. Otherwise, you're not actually running any DDL and the table copy is
333             #pod essentially a no-op.
334             #pod
335             #pod All of these triggers pass the C object as the only argument. The
336             #pod L can be acquired from that and used in SQL statements. The L
337             #pod and L methods should be used to protect against disconnections or locks.
338             #pod
339             #pod There is room to add more hooks here, but only if there's a good reason to do so.
340             #pod (Running the wrong kind of SQL at the wrong time could be dangerous.) Create a GitHub
341             #pod issue if you can think of one.
342             #pod
343             #pod =head4 before_triggers
344             #pod
345             #pod This is called before the table triggers are applied. Your DDL should take place here,
346             #pod for a few reasons:
347             #pod
348             #pod 1. The table is empty, so DDL should take no time at all now.
349             #pod
350             #pod 2. After this hook, the table is reanalyzed to make sure it has an accurate picture
351             #pod of the new columns. This is critical for the creation of the triggers.
352             #pod
353             #pod =head4 before_swap
354             #pod
355             #pod This is called after the new table has been analyzed, but before the big table swap. This
356             #pod hook might be used if a large DML operation needs to be done while the new table is still
357             #pod available. If you use this hook, it's highly recommended that you use something like
358             #pod L to make sure the changes are made in a safe and batched manner.
359             #pod
360             #pod =cut
361              
362             has coderef_hooks => (
363             is => 'ro',
364             isa => Dict[
365             before_triggers => Optional[CodeRef],
366             before_swap => Optional[CodeRef],
367             ],
368             required => 0,
369             default => sub { +{} },
370             );
371              
372             #pod =head3 copy_opts
373             #pod
374             #pod A hashref of different options to pass to L, which is used in the
375             #pod L step. Some of these are defined automatically. It's recommended that you
376             #pod specify at least these options:
377             #pod
378             #pod chunk_size => 5000, # or whatever is a reasonable size for that table
379             #pod id_name => 'pk_id', # especially if there isn't an obvious integer PK
380             #pod
381             #pod Specifying L is not recommended, since Active DBI Processing
382             #pod mode will be used.
383             #pod
384             #pod These options will be included into the hashref, unless specifically overridden by key
385             #pod name:
386             #pod
387             #pod id_name => $first_pk_column, # will warn if the PK is multi-column
388             #pod target_time => 1,
389             #pod sleep => 0.5,
390             #pod
391             #pod # If using DBIC
392             #pod dbic_storage => $rsrc->storage,
393             #pod rsc => $id_rsc,
394             #pod dbic_retry_opts => {
395             #pod max_attempts => 20,
396             #pod # best not to change this, unless you know what you're doing
397             #pod retry_handler => $onlineddl_retry_handler,
398             #pod },
399             #pod
400             #pod # If using DBI
401             #pod dbi_connector => $oddl->dbi_connector,
402             #pod min_stmt => $min_sql,
403             #pod max_stmt => $max_sql,
404             #pod
405             #pod # For both
406             #pod count_stmt => $count_sql,
407             #pod stmt => $insert_select_sql,
408             #pod progress_name => $copying_msg,
409             #pod
410             #pod =cut
411              
412             has copy_opts => (
413             is => 'ro',
414             isa => HashRef,
415             required => 0,
416             lazy => 1,
417             default => sub { {} },
418             );
419              
420             # This is filled in during copy_rows, since the _column_list call needs to happen after
421             # the DDL has run.
422             sub _fill_copy_opts {
423 27     27   73 my $self = shift;
424 27         120 my $rsrc = $self->rsrc;
425 27         116 my $dbh = $self->dbh;
426 27         991128 my $vars = $self->_vars;
427              
428 27         860 my $copy_opts = $self->copy_opts;
429 27         906 my $helper = $self->_helper;
430              
431 27         380 my $catalog = $vars->{catalog};
432 27         111 my $schema = $vars->{schema};
433 27         655 my $orig_table_name = $self->table_name;
434 27         941 my $new_table_name = $self->new_table_name;
435              
436 27         564 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
437 27         2218 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
438              
439             # Sane defaults for timing
440 27   100     864 $copy_opts->{target_time} //= 1;
441             # Copies create lots of rapid I/O, binlog generation, etc. on the primary.
442             # Some sleep time gives other servers a chance to catch up:
443 27   100     204 $copy_opts->{sleep} //= 0.5;
444              
445             # Figure out what the id_name is going to be
446             my $id_name = $copy_opts->{id_name} //= $self->dbh_runner(run => set_subname '_pk_finder', sub {
447 13     13   41 $dbh = $_;
448 13         108 my @ids = $dbh->primary_key($catalog, $schema, $orig_table_name);
449              
450 13 50       20081 die "No primary key found for $orig_table_name" unless @ids;
451 13 50       62 warn "Using the first column of a multi-column primary key for $orig_table_name" if @ids > 1;
452              
453 13         76 $ids[0];
454 27   66     320 });
455              
456 27         189 my $id_name_quote = $dbh->quote_identifier($id_name);
457              
458 27 50       1201 if ($rsrc) {
459 27   66     209 $copy_opts->{dbic_storage} //= $rsrc->storage;
460 27   66     1028 $copy_opts->{rsc} //= $rsrc->resultset->get_column($id_name);
461              
462 27   100     20048 $copy_opts->{dbic_retry_opts} //= {};
463 27   66     638 $copy_opts->{dbic_retry_opts}{max_attempts} //= $DEFAULT_MAX_ATTEMPTS;
464 27     0   873 $copy_opts->{dbic_retry_opts}{retry_handler} = sub { $self->_retry_handler(@_) };
  0         0  
465             }
466             else {
467 0   0     0 $copy_opts->{dbi_connector} //= $self->dbi_connector;
468 0   0     0 $copy_opts->{min_stmt} //= "SELECT MIN($id_name_quote) FROM $orig_table_name_quote";
469 0   0     0 $copy_opts->{max_stmt} //= "SELECT MAX($id_name_quote) FROM $orig_table_name_quote";
470             }
471              
472 27         194 my @column_list = $self->_column_list;
473 27         99 my $column_list_str = join(', ', map { $dbh->quote_identifier($_) } @column_list );
  94         2112  
474              
475             # The INSERT..SELECT is a bit different depending on the RDBMS used, mostly because
476             # of the IGNORE part
477 27         870 my $insert_select_stmt = $helper->insert_select_stmt($column_list_str);
478              
479 27   66     351 $copy_opts->{count_stmt} //= "SELECT COUNT(*) FROM $orig_table_name_quote WHERE $id_name_quote BETWEEN ? AND ?";
480 27   66     185 $copy_opts->{stmt} //= $insert_select_stmt;
481              
482 27 50 66     298 $copy_opts->{progress_name} //= "Copying $orig_table_name" unless $copy_opts->{progress_bar};
483              
484 27         188 return $copy_opts;
485             }
486              
487             #pod =head3 db_timeouts
488             #pod
489             #pod A hashref of timeouts used for various DB operations, and usually set at the beginning of
490             #pod each connection. Some of these settings may be RDBMS-specific.
491             #pod
492             #pod =head4 lock_file
493             #pod
494             #pod Amount of time (in seconds) to wait when attempting to acquire filesystem locks (on
495             #pod filesystems which support locking). Float or fractional values are allowed. This
496             #pod currently only applies to SQLite.
497             #pod
498             #pod Default value is 1 second. The downside is that the SQLite default is actually 0, so
499             #pod other (non-OnlineDDL) connections should have a setting that is more than that to prevent
500             #pod lock contention.
501             #pod
502             #pod =head4 lock_db
503             #pod
504             #pod Amount of time (in whole seconds) to wait when attempting to acquire table and/or database
505             #pod level locks before falling back to retry.
506             #pod
507             #pod Default value is 60 seconds.
508             #pod
509             #pod =head4 lock_row
510             #pod
511             #pod Amount of time (in whole seconds) to wait when attempting to acquire row-level locks,
512             #pod which apply to much lower-level operations than L. At this scope, the lesser
513             #pod of either of these two settings will take precedence.
514             #pod
515             #pod Default value is 2 seconds. Lower values are preferred for row lock wait timeouts, so
516             #pod that OnlineDDL is more likely to be the victim of lock contention. OnlineDDL can simply
517             #pod retry the connection at that point.
518             #pod
519             #pod =head4 session
520             #pod
521             #pod Amount of time (in whole seconds) for inactive session timeouts on the database side.
522             #pod
523             #pod Default value is 28,800 seconds (8 hours), which is MySQL's default.
524             #pod
525             #pod =cut
526              
527             has db_timeouts => (
528             is => 'ro',
529             isa => Dict[
530             lock_file => Optional[PositiveNum],
531             lock_db => Optional[PositiveInt],
532             lock_row => Optional[PositiveInt],
533             session => Optional[PositiveInt],
534             ],
535             required => 0,
536             );
537              
538             #pod =head3 reversible
539             #pod
540             #pod A L object, used for rollbacks. A default will be created, if not
541             #pod specified.
542             #pod
543             #pod =cut
544              
545             has reversible => (
546             is => 'rw',
547             isa => InstanceOf['Eval::Reversible'],
548             required => 1,
549             lazy => 1,
550             default => sub { Eval::Reversible->new },
551             );
552              
553             ### Private attributes
554              
555             has _vars => (
556             is => 'rw',
557             isa => HashRef,
558             required => 0,
559             init_arg => undef,
560             lazy => 1,
561             default => sub { {} },
562             );
563              
564             has _helper => (
565             is => 'ro',
566             isa => InstanceOf['DBIx::OnlineDDL::Helper::Base'],
567             required => 0,
568             init_arg => undef,
569             lazy => 1,
570             builder => '_build_helper',
571             );
572              
573             sub _build_helper {
574 27     27   427 my $self = shift;
575              
576 27         120 my $dbh = $self->dbh;
577              
578             # Get and store the DBMS_NAME. This is not the lowercase driver name (ie: mysql),
579             # unless the {Driver}{Name} alternative wins out.
580 27   33     932887 my $dbms_name = $self->_vars->{dbms_name} = $dbh->get_info( $GetInfoType{SQL_DBMS_NAME} ) // $dbh->{Driver}->{Name};
581              
582 27         1357 my $helper_class = "DBIx::OnlineDDL::Helper::$dbms_name";
583              
584             # Die if we can't load the RDBMS-specific class, since there's a lot of gaps in Base
585 27 50       211 die "OnlineDDL is not designed for $dbms_name systems yet!" unless Class::Load::load_optional_class($helper_class);
586              
587 27         4530 return $helper_class->new( online_ddl => $self );
588             }
589              
590             ### BUILD methods
591              
592             around BUILDARGS => sub {
593             my $next = shift;
594             my $class = shift;
595              
596             my %args = @_ == 1 ? %{ $_[0] } : @_;
597              
598             # Quick sanity checks
599             die 'A DBIC ResultSource (rsrc) or DBIx::Connector::Retry object (dbi_connector) is required' unless (
600             $args{rsrc} || $args{dbi_connector}
601             );
602              
603             # Defaults for db_timeouts (see POD above). We set these here, because each
604             # individual timeout should be checked to see if it's defined.
605             $args{db_timeouts} //= {};
606             $args{db_timeouts}{lock_file} //= 1;
607             $args{db_timeouts}{lock_db} //= 60;
608             $args{db_timeouts}{lock_row} //= 2;
609             $args{db_timeouts}{session} //= 28_800;
610              
611             $class->$next( %args );
612             };
613              
614             sub BUILD {
615 27     27 0 1743 my $self = shift;
616 27         115 my $rsrc = $self->rsrc;
617              
618 27         119 my $dbh = $self->dbh;
619 27         42453 my $helper = $self->_helper;
620              
621             # Get the current catalog/schema
622 27         10716 my ($catalog, $schema) = $helper->current_catalog_schema;
623              
624 27         795 $self->_vars->{catalog} = $catalog;
625 27         743 $self->_vars->{schema} = $schema;
626              
627             # Add in the post-connection details
628 27         344 my @stmts = $helper->post_connection_stmts;
629              
630 27 50       139 if ($rsrc) {
631             ### DBIC Storage
632              
633 27         97 my @post_connection_details = map { [ do_sql => $_ ] } @stmts;
  81         269  
634              
635             # XXX: Tapping into a private attribute here, but it's a lot better than parsing
636             # $storage->connect_info. We are also not attaching these details to
637             # connect_info, so public introspection won't pick up our changes. Undecided
638             # whether this is good or bad...
639              
640 27         160 my $storage = $rsrc->storage;
641 27         1657 my $on_connect_call = $storage->_dbic_connect_attributes->{on_connect_call};
642              
643             # Parse on_connect_call to make sure we can add to it
644 27   33     124 my $ref = defined $on_connect_call && ref $on_connect_call;
645 27 0       91 unless ($on_connect_call) {
    0          
    0          
    50          
646 27         77 $on_connect_call = \@post_connection_details;
647             }
648 0         0 elsif (!$ref) {
649 0         0 $on_connect_call = [ [ do_sql => $on_connect_call ], @post_connection_details ];
650             }
651 0         0 elsif ($ref eq 'ARRAY') {
652             # Double-check that we're not repeating ourselves by inspecting the array for
653             # our own statements.
654             @$on_connect_call = grep {
655 0         0 my $e = $_;
  0         0  
656             !( # exclude any of ours
657             $e && ref $e && ref $e eq 'ARRAY' && @$e == 2 &&
658             $e->[0] && !ref $e->[0] && $e->[0] eq 'do_sql' &&
659 0   0 0   0 $e->[1] && !ref $e->[1] && (any { $e->[1] eq $_ } @stmts)
  0         0  
660             );
661             } @$on_connect_call;
662              
663 0         0 my $first_occ = $on_connect_call->[0];
664 0 0 0     0 if ($first_occ && ref $first_occ && ref $first_occ eq 'ARRAY') {
      0        
665 0         0 $on_connect_call = [ @$on_connect_call, @post_connection_details ];
666             }
667             else {
668 0         0 $on_connect_call = [ $on_connect_call, @post_connection_details ];
669             }
670             }
671 0         0 elsif ($ref eq 'CODE') {
672 0         0 $on_connect_call = [ $on_connect_call, @post_connection_details ];
673             }
674             else {
675 0         0 die "Illegal reftype $ref for on_connect_call connection attribute!";
676             }
677              
678             # Set the new options on the relevant attributes that Storage::DBI->connect_info touches.
679 27         156 $storage->_dbic_connect_attributes->{on_connect_call} = $on_connect_call;
680 27         157 $storage->on_connect_call($on_connect_call);
681             }
682             else {
683             ### DBIx::Connector::Retry (via DBI Callbacks)
684              
685 0         0 my $conn = $self->dbi_connector;
686 0         0 my $dbi_attrs = $conn->connect_info->[3];
687              
688             # Playing with refs, so no need to re-set connect_info
689 0 0       0 $conn->connect_info->[3] = $dbi_attrs = {} unless $dbi_attrs;
690              
691             # Make sure the basic settings are sane
692 0         0 $dbi_attrs->{AutoCommit} = 1;
693 0         0 $dbi_attrs->{RaiseError} = 1;
694              
695             # Add the DBI callback
696 0   0     0 my $callbacks = $dbi_attrs->{Callbacks} //= {};
697 0         0 my $package_re = quotemeta(__PACKAGE__.'::_dbi_connected_callback');
698              
699 0   0     0 my $ref = defined $callbacks->{connected} && ref $callbacks->{connected};
700 0 0       0 unless ($callbacks->{connected}) {
    0          
    0          
701             $callbacks->{connected} = set_subname '_dbi_connected_callback' => sub {
702 0     0   0 shift->do($_) for @stmts;
703 0         0 return;
704 0         0 };
705             }
706 0 0       0 elsif (!$ref || $ref ne 'CODE') {
707 0         0 die "Illegal reftype $ref for connected DBI Callback!";
708             }
709 0         0 elsif (subname($callbacks->{connected}) =~ /^$package_re/) { # allow for *_wrapped below
710             # This is one of our callbacks; leave it alone!
711             }
712             else {
713             # This is somebody else's callback; wrap around it
714 0         0 my $old_coderef = $callbacks->{connected};
715             $callbacks->{connected} = set_subname '_dbi_connected_callback_wrapped' => sub {
716 0     0   0 my $h = shift;
717 0         0 $old_coderef->($h);
718 0         0 $h->do($_) for @stmts;
719 0         0 return;
720 0         0 };
721             }
722              
723             # Add a proper retry_handler
724 0     0   0 $conn->retry_handler(sub { $self->_retry_handler(@_) });
  0         0  
725              
726             # And max_attempts. XXX: Maybe they actually wanted 10 and not just the default?
727 0 0       0 $conn->max_attempts($DEFAULT_MAX_ATTEMPTS) if $conn->max_attempts == 10;
728             }
729              
730             # Go ahead and run the post-connection statements for this session
731 27         294 $dbh->{AutoCommit} = 1;
732 27         330 $dbh->{RaiseError} = 1;
733 27         253 $dbh->do($_) for @stmts;
734             }
735              
736             #pod =head1 CONSTRUCTORS
737             #pod
738             #pod See L for information on what can be passed into these constructors.
739             #pod
740             #pod =head2 new
741             #pod
742             #pod my $online_ddl = DBIx::OnlineDDL->new(...);
743             #pod
744             #pod A standard object constructor. If you use this constructor, you will need to manually
745             #pod call L to execute the DB changes.
746             #pod
747             #pod You'll probably just want to use L.
748             #pod
749             #pod =head2 construct_and_execute
750             #pod
751             #pod my $online_ddl = DBIx::OnlineDDL->construct_and_execute(...);
752             #pod
753             #pod Constructs a DBIx::OnlineDDL object and automatically calls each method step, including
754             #pod hooks. Anything passed to this method will be passed through to the constructor.
755             #pod
756             #pod Returns the constructed object, post-execution. This is typically only useful if you want
757             #pod to inspect the attributes after the process has finished. Otherwise, it's safe to just
758             #pod ignore the return and throw away the object immediately.
759             #pod
760             #pod =cut
761              
762             sub construct_and_execute {
763 0     0 1 0 my $class = shift;
764 0         0 my $online_ddl = $class->new(@_);
765              
766 0         0 $online_ddl->execute;
767              
768 0         0 return $online_ddl;
769             }
770              
771             #pod =head1 METHODS
772             #pod
773             #pod =head2 Step Runners
774             #pod
775             #pod =head3 execute
776             #pod
777             #pod Runs all of the steps as documented in L. This also includes undo
778             #pod protection, in case of exceptions.
779             #pod
780             #pod =cut
781              
782             sub execute {
783 27     27 1 2765 my $self = shift;
784 27         767 my $reversible = $self->reversible;
785              
786 27         10254 $self->_progress_bar_setup;
787              
788             $reversible->run_reversibly(set_subname '_execute_part_one', sub {
789 27     27   5160 $self->create_new_table;
790 27         3628 $self->create_triggers;
791 27         4386 $self->copy_rows;
792 27         4323 $self->swap_tables;
793 27         460 });
794             $reversible->run_reversibly(set_subname '_execute_part_two', sub {
795 27     27   4571 $self->drop_old_table;
796 27         2919 $self->cleanup_foreign_keys;
797 27         5573 });
798             }
799              
800             #pod =head3 fire_hook
801             #pod
802             #pod $online_ddl->fire_hook('before_triggers');
803             #pod
804             #pod Fires one of the coderef hooks, if it exists. This also updates the progress bar.
805             #pod
806             #pod See L for more details.
807             #pod
808             #pod =cut
809              
810             sub fire_hook {
811 54     54 1 195 my ($self, $hook_name) = @_;
812              
813 54         250 my $hooks = $self->coderef_hooks;
814 54         1036 my $vars = $self->_vars;
815              
816 54         456 my $progress = $vars->{progress_bar};
817              
818 54 100 66     572 return unless $hooks && $hooks->{$hook_name};
819              
820 15         146 $progress->message("Firing hook for $hook_name");
821              
822             # Fire the hook
823 15         863 $hooks->{$hook_name}->($self);
824              
825 15         264846 $progress->update;
826             }
827              
828             #pod =head2 DBI Helpers
829             #pod
830             #pod =head3 dbh
831             #pod
832             #pod $online_ddl->dbh;
833             #pod
834             #pod Acquires a database handle, either from L or L. Not recommended
835             #pod for active work, as it doesn't offer retry protection. Instead, use L or
836             #pod L.
837             #pod
838             #pod =cut
839              
840             sub dbh {
841 609     609   8265 my $self = shift;
842              
843             # Even acquiring a $dbh could die (eg: 'USE $db' or other pre-connect commands), so
844             # also try to wrap this in our retry handler.
845 609     609   4231 my $dbh = $self->dbh_runner( run => sub { $_[0] } );
  609         2410  
846 609         2863 return $dbh;
847             }
848              
849             #pod =head3 dbh_runner
850             #pod
851             #pod my @items = $online_ddl->dbh_runner(run => sub {
852             #pod my $dbh = $_; # or $_[0]
853             #pod $dbh->selectall_array(...);
854             #pod });
855             #pod
856             #pod Runs the C<$coderef>, locally setting C<$_> to and passing in the database handle. This
857             #pod is essentially a shortcut interface into either L or DBIC's L.
858             #pod
859             #pod The first argument can either be C or C, which controls whether to wrap the
860             #pod code in a DB transaction or not. The return is passed directly back, and return context
861             #pod is honored.
862             #pod
863             #pod =cut
864              
865             sub _retry_handler {
866 0     0   0 my ($self, $runner) = @_;
867 0         0 my $vars = $self->_vars;
868              
869             # NOTE: There's a lot of abusing the fact that BlockRunner and DBIx::Connector::Retry
870             # (a la $runner) share similar accessor interfaces.
871              
872 0         0 my $error = $runner->last_exception;
873 0         0 my $is_retryable = $self->_helper->is_error_retryable($error);
874              
875 0 0       0 if ($is_retryable) {
876 0         0 my ($failed, $max) = ($runner->failed_attempt_count, $runner->max_attempts);
877 0         0 my $progress = $vars->{progress_bar};
878              
879             # Warn about the last error
880 0 0       0 $progress->message("Encountered a recoverable error: $error") if $progress;
881              
882             # Pause for an incremental amount of seconds first, to discourage any future locks
883 0         0 sleep $failed;
884              
885             # If retries are escalating, try forcing a disconnect
886 0 0       0 if ($failed >= $max / 2) {
887             # Finally have some differences between the two classes...
888 0 0       0 if ($runner->isa('DBIx::Class::Storage::BlockRunner')) {
889 0         0 eval { $runner->storage->disconnect };
  0         0  
890             }
891             else {
892 0         0 eval { $runner->disconnect };
  0         0  
893             }
894             }
895              
896 0 0       0 $progress->message( sprintf(
897             "Attempt %u of %u", $failed, $max
898             ) ) if $progress;
899             }
900              
901 0         0 return $is_retryable;
902             }
903              
904             sub dbh_runner {
905 1123     1123 1 7402 my ($self, $method, $coderef) = @_;
906 1123         2722 my $wantarray = wantarray;
907              
908 1123 50       6715 die "Only 'txn' or 'run' are acceptable run methods" unless $method =~ /^(?:txn|run)$/;
909              
910 1123         2185 my @res;
911 1123 50       5158 if (my $rsrc = $self->rsrc) {
912             # No need to load BlockRunner, since DBIC loads it in before us if we're using
913             # this method.
914             my $block_runner = DBIx::Class::Storage::BlockRunner->new(
915             # defaults
916             max_attempts => $DEFAULT_MAX_ATTEMPTS,
917              
918             # never overrides the important ones below
919 1123         9681 %{ $self->dbic_retry_opts },
920              
921 0     0   0 retry_handler => sub { $self->_retry_handler(@_) },
922 1123 100       2008 storage => $rsrc->storage,
923             wrap_txn => ($method eq 'txn' ? 1 : 0),
924             );
925              
926             # This wrapping nonsense is necessary because Try::Tiny within BlockRunner has its own
927             # localization of $_. Fortunately, we can pass arguments to avoid closures.
928             my $wrapper = set_subname '_dbh_run_blockrunner_wrapper' => sub {
929 1123     1123   176266 my ($s, $c) = @_;
930 1123         5486 my $dbh = $s->rsrc->storage->dbh;
931              
932 1123         529548 local $_ = $dbh;
933 1123         4590 $c->($dbh); # also pass it in, because that's what DBIx::Connector does
934 1123         138802 };
935              
936             # BlockRunner can still die post-failure, if $storage->ensure_connected (which calls ping
937             # and tries to reconnect) dies. If that's the case, use our retry handler to check the new
938             # error message, and throw it back into BlockRunner.
939 1123         2835 my $br_method = 'run';
940 1123         24120 while ($block_runner->failed_attempt_count < $block_runner->max_attempts) {
941 1123         13722 eval {
942 1123 100       3631 unless (defined $wantarray) { $block_runner->$br_method($wrapper, $self, $coderef) }
  339 100       1593  
943 0         0 elsif ($wantarray) { @res = $block_runner->$br_method($wrapper, $self, $coderef) }
  135         800  
944 649         3012 else { $res[0] = $block_runner->$br_method($wrapper, $self, $coderef) }
945             };
946              
947             # 'run' resets failed_attempt_count, so subsequent attempts must use
948             # '_run', which does not
949 1123         2595739 $br_method = '_run';
950              
951 1123 50       3730 if (my $err = $@) {
952             # Time to really die
953 0 0 0     0 die $err if $err =~ /Reached max_attempts amount of / || $block_runner->failed_attempt_count >= $block_runner->max_attempts;
954              
955             # See if the retry handler likes it
956 0         0 push @{ $block_runner->exception_stack }, $err;
  0         0  
957 0         0 $block_runner->_set_failed_attempt_count( $block_runner->failed_attempt_count + 1 );
958 0 0       0 die $err unless $self->_retry_handler($block_runner);
959             }
960             else {
961 1123         9573 last;
962             }
963             }
964             }
965             else {
966 0         0 my $conn = $self->dbi_connector;
967 0 0       0 unless (defined $wantarray) { $conn->$method($coderef) }
  0 0       0  
968 0         0 elsif ($wantarray) { @res = $conn->$method($coderef) }
  0         0  
969 0         0 else { $res[0] = $conn->$method($coderef) }
970             }
971              
972 1123 100       5487 return $wantarray ? @res : $res[0];
973             }
974              
975             #pod =head3 dbh_runner_do
976             #pod
977             #pod $online_ddl->dbh_runner_do(
978             #pod "ALTER TABLE $table_name ADD COLUMN foobar",
979             #pod ["ALTER TABLE ? DROP COLUMN ?", undef, $table_name, 'baz'],
980             #pod );
981             #pod
982             #pod Runs a list of commands, encapsulating each of them in a L coderef with calls
983             #pod to L. This is handy when you want to run a list of DDL commands, which you don't
984             #pod care about the output of, but don't want to bundle them into a single non-idempotant
985             #pod repeatable coderef. Or if you want to save typing on a single do-able SQL command.
986             #pod
987             #pod The items can either be a SQL string or an arrayref of options to pass to L.
988             #pod
989             #pod The statement is assumed to be non-transactional. If you want to run a DB transaction,
990             #pod you should use L instead.
991             #pod
992             #pod =cut
993              
994             sub dbh_runner_do {
995 176     176 1 499052 my ($self, @commands) = @_;
996              
997 176         594 foreach my $command (@commands) {
998 176         525 my $ref = ref $command;
999 176 50 33     792 die "$ref references not valid in dbh_runner_do" if $ref && $ref ne 'ARRAY';
1000              
1001             $self->dbh_runner(run => set_subname '_dbh_runner_do', sub {
1002 176 50   176   1376 $_->do( $ref ? @$command : $command );
1003 176         2129 });
1004             }
1005             }
1006              
1007             #pod =head1 STEP METHODS
1008             #pod
1009             #pod You can call these methods individually, but using L instead is
1010             #pod highly recommended. If you do run these yourself, the exception will need to be caught
1011             #pod and the L undo stack should be run to get the DB back to normal.
1012             #pod
1013             #pod =head2 create_new_table
1014             #pod
1015             #pod Creates the new table, making sure to preserve as much of the original table properties
1016             #pod as possible.
1017             #pod
1018             #pod =cut
1019              
1020             sub create_new_table {
1021 27     27 1 88 my $self = shift;
1022 27         134 my $dbh = $self->dbh;
1023 27         914815 my $vars = $self->_vars;
1024              
1025 27         264 my $progress = $vars->{progress_bar};
1026 27         523 my $reversible = $self->reversible;
1027 27         687 my $helper = $self->_helper;
1028              
1029 27         736 my $orig_table_name = $self->table_name;
1030 27         764 my $new_table_name = $self->new_table_name;
1031              
1032 27         952 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
1033 27         1505 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
1034              
1035             # ANSI quotes could also appear in the statement
1036 27         673 my $orig_table_name_ansi_quote = '"'.$orig_table_name.'"';
1037              
1038 27         281 $progress->message("Creating new table $new_table_name");
1039              
1040 27         1463 my $table_sql = $helper->create_table_sql($orig_table_name);
1041 27 50       144 die "Table $orig_table_name does not exist in the database!" unless $table_sql;
1042              
1043 27 50       170 $table_sql = $helper->rename_fks_in_table_sql($orig_table_name, $table_sql) if $helper->dbms_uses_global_fk_namespace;
1044              
1045             # Change the old->new table name
1046 27         206 my $orig_table_name_quote_re = '('.join('|',
1047             quotemeta($orig_table_name_quote), quotemeta($orig_table_name_ansi_quote), quotemeta($orig_table_name)
1048             ).')';
1049 27         973 $table_sql =~ s/(?<=^CREATE TABLE )$orig_table_name_quote_re/$new_table_name_quote/;
1050              
1051             # NOTE: This SQL will still have the old table name in self-referenced FKs. This is
1052             # okay, since no supported RDBMS currently auto-renames the referenced table name
1053             # during table moves, and the old table is still the definitive point-of-record until
1054             # the table swap. Furthermore, pointing the FK to the new table may cause bad FK
1055             # constraint failures within the triggers, if the referenced ID hasn't been copied to
1056             # the new table yet.
1057             #
1058             # If we ever have a RDBMS that does some sort of auto-renaming of FKs, we'll need to
1059             # accommodate it. It's also worth noting that turning FKs on during the session can
1060             # actually affect this kind of behavior. For example, both MySQL & SQLite will rename
1061             # them during table swaps, but only if the FK checks are on.
1062              
1063             # Actually create the table
1064 27         201 $self->dbh_runner_do($table_sql);
1065              
1066             # Undo commands, including a failure warning update
1067 27         1319 $reversible->failure_warning("\nDropping the new table and rolling back to start!\n\n");
1068 27     0   2185 $reversible->add_undo(sub { $self->dbh_runner_do("DROP TABLE $new_table_name_quote") });
  0         0  
1069              
1070 27         2228 $progress->update;
1071             }
1072              
1073             #pod =head2 create_triggers
1074             #pod
1075             #pod Creates triggers on the original table to make sure any new changes are captured into the
1076             #pod new table.
1077             #pod
1078             #pod =cut
1079              
1080             sub create_triggers {
1081 27     27 1 71 my $self = shift;
1082 27         110 my $rsrc = $self->rsrc;
1083 27         169 my $dbh = $self->dbh;
1084 27         952380 my $vars = $self->_vars;
1085              
1086 27         305 my $progress = $vars->{progress_bar};
1087 27         560 my $reversible = $self->reversible;
1088 27         802 my $helper = $self->_helper;
1089              
1090 27         310 my $catalog = $vars->{catalog};
1091 27         145 my $schema = $vars->{schema};
1092 27         610 my $orig_table_name = $self->table_name;
1093 27         834 my $new_table_name = $self->new_table_name;
1094              
1095             # Fire the before_triggers hook, which would typically include the DDL
1096 27         425 $self->fire_hook('before_triggers');
1097              
1098 27         1281 $progress->message("Creating triggers");
1099              
1100             # This shouldn't be cached until now, since the actual DDL may change the column list
1101 27         1274 my @column_list = $self->_column_list;
1102              
1103             ### Look for a unique ID set
1104              
1105             # We need to find a proper PK or unique constraint for UPDATE/DELETE triggers.
1106             # Unlike BatchChunker, we can't just rely on part of a PK.
1107 27         66 my @unique_ids;
1108             $self->dbh_runner(run => set_subname '_unique_id_finder', sub {
1109 27     27   114 $dbh = $_;
1110              
1111 27         1556 my %potential_unique_ids = (
1112             PRIMARY => [ $dbh->primary_key($catalog, $schema, $orig_table_name) ],
1113             );
1114              
1115 27         31597 my $unique_stats = [];
1116 27 50       247 if ($dbh->can('statistics_info')) {
1117             # Sometimes, this still dies, even with the 'can' check (eg: older DBD::mysql drivers)
1118 27         65 $unique_stats = eval { $dbh->statistics_info( $catalog, $schema, $orig_table_name, 1, 1 )->fetchall_arrayref({}) };
  27         184  
1119 27 50       39193 $unique_stats = [] if $@;
1120             }
1121              
1122 27         123 foreach my $index_name (uniq map { $_->{INDEX_NAME} } @$unique_stats) {
  66         282  
1123             my @unique_cols =
1124 66         159 map { $_->{COLUMN_NAME} }
1125 36         128 sort { $a->{ORDINAL_POSITION} <=> $b->{ORDINAL_POSITION} }
1126 34 100       106 grep { $_->{INDEX_NAME} eq $index_name && !$_->{NON_UNIQUE} } # some DBDs might not honor the $unique_only param
  152         670  
1127             @$unique_stats
1128             ;
1129 34         124 $potential_unique_ids{$index_name} = \@unique_cols;
1130             }
1131              
1132 27         78 my %column_set = map { $_ => 1 } @column_list;
  94         238  
1133 27         148 foreach my $index_name ('PRIMARY',
1134             # sort by the number of columns (asc), though PRIMARY still has top priority
1135 15         30 sort { scalar(@{$potential_unique_ids{$a}}) <=> scalar(@{$potential_unique_ids{$b}}) }
  15         36  
  15         50  
1136 61         196 grep { $_ ne 'PRIMARY' }
1137             keys %potential_unique_ids
1138             ) {
1139 61         112 my @unique_cols = @{ $potential_unique_ids{$index_name} };
  61         186  
1140 61 50       172 next unless @unique_cols;
1141              
1142             # Only use this set if all of the columns exist in both tables
1143 61 50       326 next unless all { $column_set{$_} } @unique_cols;
  97         257  
1144              
1145 61         430 @unique_ids = @unique_cols;
1146             }
1147 27         466 });
1148              
1149 27 50       384 die "Cannot find an appropriate unique index for $orig_table_name!" unless @unique_ids;
1150              
1151             ### Check to make sure existing triggers aren't on the table
1152              
1153 27 50       184 die "Found triggers on $orig_table_name! Please remove them first, so that our INSERT/UPDATE/DELETE triggers can be applied."
1154             if $helper->has_triggers_on_table($orig_table_name);
1155              
1156             ### Find a good set of trigger names
1157              
1158 27         151 foreach my $trigger_type (qw< INSERT UPDATE DELETE >) {
1159 81         2543 my $trigger_name = $helper->find_new_trigger_identifier(
1160             "${orig_table_name}_onlineddl_".lc($trigger_type)
1161             );
1162 81         607 $vars->{trigger_names} {$trigger_type} = $trigger_name;
1163 81         653 $vars->{trigger_names_quoted}{$trigger_type} = $dbh->quote_identifier($trigger_name);
1164             }
1165              
1166             ### Now create the triggers, with (mostly) ANSI SQL
1167              
1168 27         1235 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
1169 27         759 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
1170              
1171 27         710 my $column_list_str = join(', ', map { $dbh->quote_identifier($_) } @column_list );
  94         1796  
1172 27         722 my $new_column_list_str = join(', ', map { "NEW.".$dbh->quote_identifier($_) } @column_list );
  94         1765  
1173              
1174 27         783 my $nseo = $helper->null_safe_equals_op;
1175 27         66 my %trigger_dml_stmts;
1176              
1177             # Using REPLACE just in case the row already exists from the copy
1178 27         225 $trigger_dml_stmts{replace} = join("\n",
1179             "REPLACE INTO $new_table_name_quote",
1180             " ($column_list_str)",
1181             "VALUES",
1182             " ($new_column_list_str)",
1183             );
1184              
1185             my $update_unique_where_str = join(' AND ',
1186             (map {
1187 27         84 join(
  46         1307  
1188             # Use NULL-safe equals, since unique indexes could be nullable
1189             " $nseo ",
1190             "OLD.".$dbh->quote_identifier($_),
1191             "NEW.".$dbh->quote_identifier($_),
1192             );
1193             } @unique_ids)
1194             );
1195              
1196             my $delete_unique_where_str = join(' AND ',
1197             (map {
1198 27         1311 join(
  46         1166  
1199             # Use NULL-safe equals, since unique indexes could be nullable
1200             " $nseo ",
1201             "$new_table_name_quote.".$dbh->quote_identifier($_),
1202             "OLD.".$dbh->quote_identifier($_),
1203             );
1204             } @unique_ids)
1205             );
1206              
1207             # For the UPDATE trigger, DELETE the row, but only if the unique IDs have been
1208             # changed. The "NOT ($update_unique_where_str)" part keeps from deleting rows where
1209             # the unique ID is untouched.
1210 27         1435 $trigger_dml_stmts{delete_for_update} = join("\n",
1211             "DELETE FROM $new_table_name_quote WHERE",
1212             " NOT ($update_unique_where_str) AND",
1213             " $delete_unique_where_str"
1214             );
1215              
1216 27         171 $trigger_dml_stmts{delete_for_delete} = join("\n",
1217             "DELETE FROM $new_table_name_quote WHERE",
1218             " $delete_unique_where_str"
1219             );
1220              
1221 27         222 $helper->modify_trigger_dml_stmts( \%trigger_dml_stmts );
1222              
1223 27         83 foreach my $trigger_type (qw< INSERT UPDATE DELETE >) {
1224             my $trigger_header = join(' ',
1225 81         4847 "CREATE TRIGGER ".$vars->{trigger_names_quoted}{$trigger_type},
1226             "AFTER $trigger_type ON $orig_table_name_quote FOR EACH ROW"
1227             );
1228              
1229             # Even though some of these are just a single SQL statement, not every RDBMS
1230             # (like SQLite) supports leaving out the BEGIN/END keywords.
1231 81         264 my $trigger_sql = join("\n",
1232             $trigger_header,
1233             "BEGIN",
1234             '',
1235             );
1236              
1237 81 100       522 if ($trigger_type eq 'INSERT') {
    100          
    50          
1238             # INSERT trigger: Just a REPLACE command
1239 27         104 $trigger_sql .= $trigger_dml_stmts{replace}.';';
1240             }
1241             elsif ($trigger_type eq 'UPDATE') {
1242             # UPDATE trigger: DELETE special unique ID changes, then another REPLACE command.
1243             $trigger_sql .= join("\n",
1244             $trigger_dml_stmts{delete_for_update}.';',
1245 27         215 $trigger_dml_stmts{replace}.';',
1246             );
1247             }
1248             elsif ($trigger_type eq 'DELETE') {
1249             # DELETE trigger: Just a DELETE command
1250 27         116 $trigger_sql .= $trigger_dml_stmts{delete_for_delete}.';';
1251             }
1252 81         184 $trigger_sql .= "\nEND";
1253              
1254             # DOIT!
1255 81         357 $self->dbh_runner_do($trigger_sql);
1256              
1257             $reversible->add_undo(sub {
1258 0     0   0 $self->dbh_runner_do( "DROP TRIGGER IF EXISTS ".$self->_vars->{trigger_names_quoted}{$trigger_type} );
1259 81         3877 });
1260             }
1261              
1262 27         2135 $progress->update;
1263             }
1264              
1265             #pod =head2 copy_rows
1266             #pod
1267             #pod Fires up a L process to copy all of the rows from the old table to
1268             #pod the new.
1269             #pod
1270             #pod =cut
1271              
1272             sub copy_rows {
1273 27     27 1 79 my $self = shift;
1274 27         208 my $dbh = $self->dbh;
1275 27         985052 my $vars = $self->_vars;
1276              
1277 27         312 my $progress = $vars->{progress_bar};
1278 27         158 my $copy_opts = $self->_fill_copy_opts;
1279              
1280 27         287 $progress->message("Copying all rows to the new table");
1281              
1282 27         1859 DBIx::BatchChunker->construct_and_execute( %$copy_opts );
1283 27         72459104 $vars->{new_table_copied} = 1;
1284              
1285             # Analyze the table, since we have a ton of new rows now
1286 27         227 $progress->message("Analyzing table");
1287 27         1820 $self->_helper->analyze_table( $self->new_table_name );
1288              
1289 27         241 $progress->update;
1290             }
1291              
1292             #pod =head2 swap_tables
1293             #pod
1294             #pod With the new table completely modified and set up, this swaps the old/new tables.
1295             #pod
1296             #pod =cut
1297              
1298             sub swap_tables {
1299 27     27 1 94 my $self = shift;
1300 27         147 my $dbh = $self->dbh;
1301 27         1024142 my $vars = $self->_vars;
1302              
1303 27         368 my $progress = $vars->{progress_bar};
1304 27         598 my $reversible = $self->reversible;
1305 27         754 my $helper = $self->_helper;
1306              
1307 27         319 my $catalog = $vars->{catalog};
1308 27         99 my $schema = $vars->{schema};
1309 27         550 my $orig_table_name = $self->table_name;
1310 27         790 my $new_table_name = $self->new_table_name;
1311              
1312 27   50     527 my $escape = $dbh->get_info( $GetInfoType{SQL_SEARCH_PATTERN_ESCAPE} ) // '\\';
1313              
1314             # Fire the before_swap hook
1315 27         679 $self->fire_hook('before_swap');
1316              
1317 27 50 33     299 if ($helper->dbms_uses_global_fk_namespace || $helper->child_fks_need_adjusting) {
1318             # The existing parent/child FK list needs to be captured prior to the swap. The FKs
1319             # have already been created, and possibly changed/deleted, from the new table, so we
1320             # use that as reference. They have *not* been re-created on the child tables, so
1321             # the original table is used as reference.
1322 0   0     0 my $fk_hash = $vars->{foreign_keys}{definitions} //= {};
1323             $self->dbh_runner(run => set_subname '_fk_info_query', sub {
1324 0     0   0 $dbh = $_;
1325 0         0 $fk_hash->{parent} = $self->_fk_info_to_hash( $helper->foreign_key_info(undef, undef, undef, $catalog, $schema, $new_table_name) );
1326 0         0 $fk_hash->{child} = $self->_fk_info_to_hash( $helper->foreign_key_info($catalog, $schema, $orig_table_name, undef, undef, undef) );
1327 0         0 });
1328             }
1329              
1330             # Find an "_old" table name first
1331             my $old_table_name = $vars->{old_table_name} = $self->_find_new_identifier(
1332             "_${orig_table_name}_old" => set_subname('_old_table_name_finder', sub {
1333 27     27   122 my ($d, $like_expr) = @_;
1334 27         413 $like_expr =~ s/([_%])/$escape$1/g;
1335              
1336 27         265 $d->table_info($catalog, $schema, $like_expr)->fetchrow_array;
1337 27         506 }),
1338             'SQL_MAXIMUM_TABLE_NAME_LENGTH',
1339             );
1340 27         341 my $old_table_name_quote = $dbh->quote_identifier($old_table_name);
1341              
1342 27         1711 $progress->message("Swapping tables ($new_table_name --> $orig_table_name --> $old_table_name)");
1343              
1344             # Let's swap tables!
1345 27         1681 $helper->swap_tables($new_table_name, $orig_table_name, $old_table_name);
1346              
1347             # Kill the undo stack now, just in case something weird happens between now and the
1348             # end of the reversibly block. We've reached a "mostly successful" state, so rolling
1349             # back here would be undesirable.
1350 27         1665 $reversible->clear_undo;
1351 27         2661 $vars->{new_table_swapped} = 1;
1352              
1353 27         232 $progress->update;
1354             }
1355              
1356             #pod =head2 drop_old_table
1357             #pod
1358             #pod Drops the old table. This will also remove old foreign keys on child tables. (Those FKs
1359             #pod are re-applied to the new table in the next step.)
1360             #pod
1361             #pod =cut
1362              
1363             sub drop_old_table {
1364 27     27 1 86 my $self = shift;
1365 27         189 my $dbh = $self->dbh;
1366 27         884301 my $vars = $self->_vars;
1367              
1368 27         309 my $progress = $vars->{progress_bar};
1369 27         556 my $reversible = $self->reversible;
1370 27         906 my $helper = $self->_helper;
1371              
1372 27         312 my $old_table_name = $vars->{old_table_name};
1373 27         223 my $old_table_name_quote = $dbh->quote_identifier($old_table_name);
1374              
1375 27         1938 $reversible->failure_warning( join "\n",
1376             '',
1377             "The new table has been swapped, but since the process was interrupted, foreign keys will",
1378             "need to be cleaned up, and the old table dropped.",
1379             '',
1380             );
1381              
1382             # The RDBMS may complain about dangling non-functional FKs if the DROP happens first,
1383             # so let's remove those child FKs first, and reapply them later. We turn off FK
1384             # checks, so these constraint drops are quick and low risk.
1385             #
1386             # SQLite doesn't actually support DROP CONSTRAINT, but it doesn't do any messy business with
1387             # FK renames, either. So, SQLite can just skip this step.
1388 27 50       1298 if ($helper->child_fks_need_adjusting) {
1389 0         0 $progress->message("Removing FKs from child tables");
1390              
1391 0         0 $self->dbh_runner_do(
1392             $helper->remove_fks_from_child_tables_stmts
1393             );
1394             }
1395              
1396             # Now, the actual DROP
1397 27         328 $progress->message("Dropping old table $old_table_name");
1398              
1399 27         1430 $self->dbh_runner_do("DROP TABLE $old_table_name_quote");
1400              
1401 27         279 $progress->update;
1402             }
1403              
1404             #pod =head2 cleanup_foreign_keys
1405             #pod
1406             #pod Clean up foreign keys on both the new and child tables.
1407             #pod
1408             #pod =cut
1409              
1410             sub cleanup_foreign_keys {
1411 27     27 1 78 my $self = shift;
1412 27         231 my $dbh = $self->dbh;
1413 27         894477 my $vars = $self->_vars;
1414              
1415 27         307 my $progress = $vars->{progress_bar};
1416 27         553 my $reversible = $self->reversible;
1417 27         707 my $helper = $self->_helper;
1418              
1419 27         811 $reversible->failure_warning( join "\n",
1420             '',
1421             "The new table is live, but since the process was interrupted, foreign keys will need to be",
1422             "cleaned up.",
1423             '',
1424             );
1425              
1426 27 50       1257 if ($helper->dbms_uses_global_fk_namespace) {
1427             # The DB has global namespaces for foreign keys, so we are renaming them back to
1428             # their original names. The original table has already been dropped, so there's
1429             # no more risk of bumping into that namespace.
1430 0         0 $progress->message("Renaming parent FKs back to the original constraint names");
1431              
1432 0         0 $self->dbh_runner_do(
1433             $helper->rename_fks_back_to_original_stmts
1434             );
1435             }
1436              
1437 27 50       167 if ($helper->child_fks_need_adjusting) {
1438             # Since we captured the child FK names prior to the swap, they should have the
1439             # original FK names, even before MySQL's "helpful" changes on "${tbl_name}_ibfk_" FK
1440             # names.
1441 0         0 $progress->message("Adding FKs back on child tables");
1442              
1443 0         0 $self->dbh_runner_do(
1444             $helper->add_fks_back_to_child_tables_stmts
1445             );
1446             }
1447              
1448 27         174 $progress->update;
1449             }
1450              
1451             ### Private methods
1452              
1453             sub _find_new_identifier {
1454 135     135   3309 my ($self, $desired_identifier, $finder_sub, $length_info_str) = @_;
1455 135   100     791 $length_info_str ||= 'SQL_MAXIMUM_IDENTIFIER_LENGTH';
1456              
1457 135         377 state $hash_digits = ['a' .. 'z', '0' .. '9'];
1458              
1459 135         494 my $hash = join '', map { $hash_digits->[rand @$hash_digits] } 1 .. 10;
  1350         3645  
1460              
1461             # Test out some potential names
1462 135         1050 my @potential_names = (
1463             $desired_identifier, "_${desired_identifier}",
1464             "${desired_identifier}_${hash}", "_${desired_identifier}_${hash}",
1465             $hash, "_${hash}"
1466             );
1467              
1468 135   50     626 my $max_len = $self->dbh->get_info( $GetInfoType{$length_info_str} ) || 256;
1469              
1470 135         4780517 my $new_name;
1471 135         531 foreach my $potential_name (@potential_names) {
1472 135         581 $potential_name = substr($potential_name, 0, $max_len); # avoid the ID name character limit
1473              
1474             my @results = $self->dbh_runner(run => set_subname '_find_new_identifier_dbh_runner', sub {
1475 135     135   686 $finder_sub->($_, $potential_name);
1476 135         1947 });
1477              
1478             # Skip if we found it
1479 135 50       887 next if @results;
1480              
1481 135         376 $new_name = $potential_name;
1482 135         368 last;
1483             }
1484              
1485             # This really shouldn't happen...
1486 135 50       604 die "Cannot find a proper identifier name for $desired_identifier! All of them are taken!" unless defined $new_name;
1487              
1488 135         1756 return $new_name;
1489             }
1490              
1491             sub _column_list {
1492 54     54   176 my $self = shift;
1493 54         281 my $dbh = $self->dbh;
1494 54         1926922 my $vars = $self->_vars;
1495              
1496 54         697 my $catalog = $vars->{catalog};
1497 54         188 my $schema = $vars->{schema};
1498 54         1281 my $orig_table_name = $self->table_name;
1499 54         1582 my $new_table_name = $self->new_table_name;
1500              
1501 54         638 my (@old_column_list, @new_column_list);
1502             $self->dbh_runner(run => set_subname '_column_list_runner', sub {
1503 54     54   165 $dbh = $_;
1504             @old_column_list =
1505 188         96164 map { $_->{COLUMN_NAME} }
1506 54         164 @{ $dbh->column_info( $catalog, $schema, $orig_table_name, '%' )->fetchall_arrayref({ COLUMN_NAME => 1 }) }
  54         499  
1507             ;
1508             @new_column_list =
1509 216         66812 map { $_->{COLUMN_NAME} }
1510 54         236 @{ $dbh->column_info( $catalog, $schema, $new_table_name, '%' )->fetchall_arrayref({ COLUMN_NAME => 1 }) }
  54         367  
1511             ;
1512 54         845 });
1513              
1514             # We only care about columns that exist in both tables. If a column was added on the
1515             # new table, there's no data to copy. If a column was deleted from the new table, we
1516             # don't care about keeping it.
1517 54         371 my %new_column_set = map { $_ => 1 } @new_column_list;
  216         644  
1518 54         180 return grep { $new_column_set{$_} } @old_column_list;
  188         609  
1519             }
1520              
1521             sub _fk_info_to_hash {
1522 0     0     my ($self, $fk_sth) = @_;
1523 0           my $vars = $self->_vars;
1524 0           my $dbh = $self->dbh;
1525              
1526             # NOTE: Need to account for alternate ODBC names
1527              
1528 0           my @fk_rows = @{ $fk_sth->fetchall_arrayref({}) };
  0            
1529             @fk_rows = sort {
1530             # Sort by FK name, then by the column sequence number
1531 0           $a->{FK_NAME} cmp $b->{FK_NAME} ||
1532             ($a->{KEY_SEQ} // $a->{ORDINAL_POSITION}) <=> ($a->{KEY_SEQ} // $a->{ORDINAL_POSITION})
1533 0 0 0       } @fk_rows;
      0        
1534              
1535 0           my (%fks, %create_table_sql);
1536 0           foreach my $row (@fk_rows) {
1537             # Some of these rows aren't even FKs
1538 0 0 0       next unless $row->{PKTABLE_NAME} || $row->{UK_TABLE_CAT};
1539 0 0 0       next unless $row->{FKTABLE_NAME} || $row->{FK_TABLE_NAME};
1540              
1541 0   0       my $fk_name = $row->{FK_NAME} // $row->{FKCOLUMN_NAME};
1542 0   0       my $fk_table_name = $row->{FKTABLE_NAME} // $row->{FK_TABLE_NAME};
1543              
1544             my $key = join( '.',
1545             $row->{PKTABLE_NAME} // $row->{UK_TABLE_CAT},
1546 0   0       $fk_name,
1547             );
1548              
1549             # Since there may be multiple columns per FK, those associated columns are
1550             # arrayrefs.
1551 0 0         unless ($fks{$key}) {
1552              
1553             $fks{$key} = {
1554             fk_name => $fk_name,
1555              
1556             # The table where the original PK exists
1557             pk_table_name => $row->{PKTABLE_NAME} // $row->{UK_TABLE_CAT},
1558             pk_columns => [ $row->{PKCOLUMN_NAME} // $row->{UK_COLUMN_NAME} ],
1559              
1560             # The table where the FK constraint has been declared
1561             fk_table_name => $fk_table_name,
1562 0   0       fk_columns => [ $row->{FKCOLUMN_NAME} // $row->{FK_COLUMN_NAME} ],
      0        
      0        
1563             };
1564              
1565             # Sadly, foreign_key_info doesn't always fill in all of the details for the FK, so the
1566             # CREATE TABLE SQL is actually the better record. Fortunately, this is all ANSI SQL.
1567 0   0       my $create_table_sql = $create_table_sql{$fk_table_name} //= $self->_helper->create_table_sql($fk_table_name);
1568 0           my $fk_name_quote_re = '(?:'.join('|',
1569             quotemeta( $dbh->quote_identifier($fk_name) ), quotemeta('"'.$fk_name.'"'), quotemeta($fk_name)
1570             ).')';
1571              
1572 0 0         if ($create_table_sql =~ m<
1573             CONSTRAINT \s $fk_name_quote_re \s ( # start capture of full SQL
1574             FOREIGN \s KEY \s \( [^\)]+ \) \s # "FOREIGN KEY" plus column list (which we already have above)
1575             REFERENCES \s [^\(]+ \s \( [^\)]+ \) # "REFERENCES" plus table+column list (again, already captured above)
1576             \s? ( [^\)\,]* ) # ON DELETE/UPDATE, DEFER, MATCH, etc.
1577             ) # end capture of full SQL
1578             >isx) {
1579 0           my ($fk_sql, $extra_sql) = ($1, $2);
1580 0           $fk_sql =~ s/^\s+|\s+$//g;
1581              
1582 0           $fks{$key}{fk_sql} = $fk_sql;
1583 0 0         $fks{$key}{delete_rule} = $1 if $extra_sql =~ /ON DELETE ((?:SET |NO )?\w+)/i;
1584 0 0         $fks{$key}{update_rule} = $1 if $extra_sql =~ /ON UPDATE ((?:SET |NO )?\w+)/i;
1585 0 0         $fks{$key}{defer} = $1 if $extra_sql =~ /((?:NOT )?DEFERRABLE(?: INITIALLY \w+)?)/i;
1586 0 0         $fks{$key}{match} = $1 if $extra_sql =~ /(MATCH \w+)/i;
1587             }
1588             }
1589             else {
1590 0   0       push @{ $fks{$key}{pk_columns} }, $row->{PKCOLUMN_NAME} // $row->{UK_COLUMN_NAME};
  0            
1591 0   0       push @{ $fks{$key}{fk_columns} }, $row->{FKCOLUMN_NAME} // $row->{FK_COLUMN_NAME};
  0            
1592             }
1593             }
1594              
1595 0           return \%fks;
1596             }
1597              
1598             sub _fk_to_sql {
1599 0     0     my ($self, $fk) = @_;
1600 0           my $dbh = $self->dbh;
1601              
1602             # Everything after the CONSTRAINT keyword (ANSI SQL)
1603              
1604 0 0         if ($fk->{fk_sql}) {
1605             # Already have most of the SQL
1606             return join(' ',
1607             $dbh->quote_identifier($fk->{fk_name}),
1608             $fk->{fk_sql},
1609 0           );
1610             }
1611              
1612             return join(' ',
1613             $dbh->quote_identifier($fk->{fk_name}),
1614             'FOREIGN KEY',
1615 0           '('.join(', ', map { $dbh->quote_identifier($_) } @{ $fk->{fk_columns} }).')',
  0            
1616             'REFERENCES',
1617             $dbh->quote_identifier($fk->{pk_table_name}),
1618 0           '('.join(', ', map { $dbh->quote_identifier($_) } @{ $fk->{pk_columns} }).')',
  0            
1619             ( $fk->{match} ? $fk->{match} : () ),
1620             ( $fk->{delete_rule} ? 'ON DELETE '.$fk->{delete_rule} : () ),
1621             ( $fk->{update_rule} ? 'ON UPDATE '.$fk->{update_rule} : () ),
1622 0 0         ( $fk->{defer} ? $fk->{defer} : () ),
    0          
    0          
    0          
1623             );
1624             }
1625              
1626             #pod =head1 SEE ALSO
1627             #pod
1628             #pod =over
1629             #pod
1630             #pod =item *
1631             #pod
1632             #pod L
1633             #pod
1634             #pod =item *
1635             #pod
1636             #pod L
1637             #pod
1638             #pod =item *
1639             #pod
1640             #pod L
1641             #pod
1642             #pod =item *
1643             #pod
1644             #pod L
1645             #pod
1646             #pod =back
1647             #pod
1648             #pod =head1 WHY YET ANOTHER OSC?
1649             #pod
1650             #pod The biggest reason is that none of the above fully support foreign key constraints.
1651             #pod Percona's C comes close, but also includes this paragraph:
1652             #pod
1653             #pod Due to a limitation in MySQL, foreign keys will not have the same names after the ALTER
1654             #pod that they did prior to it. The tool has to rename the foreign key when it redefines it,
1655             #pod which adds a leading underscore to the name. In some cases, MySQL also automatically
1656             #pod renames indexes required for the foreign key.
1657             #pod
1658             #pod So, tables swapped with C are not exactly what they used to be before the swap.
1659             #pod It also had a number of other quirks that just didn't work out for us, related to FKs and
1660             #pod the amount of switches required to make it (semi-)work.
1661             #pod
1662             #pod Additionally, by making DBIx::OnlineDDL its own Perl module, it's a lot easier to run
1663             #pod Perl-based schema changes along side L without having to switch
1664             #pod between Perl and CLI. If other people want to subclass this module for their own
1665             #pod environment-specific quirks, they have the power to do so, too.
1666             #pod
1667             #pod =cut
1668              
1669             1;
1670              
1671             __END__