File Coverage

blib/lib/DBIx/OnlineDDL.pm
Criterion Covered Total %
statement 391 516 75.7
branch 47 140 33.5
condition 32 128 25.0
subroutine 50 61 81.9
pod 11 12 91.6
total 531 857 61.9


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   1567967 use version;
  2         20  
  2         16  
6             our $VERSION = 'v0.940.0'; # VERSION
7              
8 2     2   166 use v5.10;
  2         7  
9 2     2   9 use Moo;
  2         3  
  2         12  
10 2     2   639 use MooX::StrictConstructor;
  2         3  
  2         16  
11              
12 2     2   1636 use Types::Standard qw( Str Bool HashRef CodeRef InstanceOf Dict Optional );
  2         3  
  2         18  
13 2     2   3725 use Types::Common::Numeric qw( PositiveNum PositiveInt );
  2         20518  
  2         16  
14              
15 2     2   1968 use Class::Load;
  2         18953  
  2         91  
16 2     2   719 use DBI::Const::GetInfoType;
  2         9905  
  2         280  
17 2     2   20 use DBIx::BatchChunker 0.92; # with stmt attrs
  2         56  
  2         43  
18 2     2   940 use Eval::Reversible;
  2         51641  
  2         127  
19 2     2   17 use List::Util 1.44 (qw( uniq any all first )); # 1.44 has uniq
  2         53  
  2         134  
20 2     2   12 use Sub::Util qw( subname set_subname );
  2         5  
  2         104  
21 2     2   13 use Term::ProgressBar 2.14; # with silent option
  2         33  
  2         77  
22              
23             # Don't export the above, but don't conflict with StrictConstructor, either
24 2     2   12 use namespace::clean -except => [qw< new meta >];
  2         4  
  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 28     28   9277 my $self = shift;
250 28         118 my $dbh = $self->dbh;
251 28         981455 my $vars = $self->_vars;
252              
253 28         236 my $catalog = $vars->{catalog};
254 28         90 my $schema = $vars->{schema};
255 28         515 my $orig_table_name = $self->table_name;
256              
257 28   50     559 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 28     28   67 $dbh = shift;
262 28         925 my $like_expr = shift;
263 28         384 $like_expr =~ s/([_%])/$escape$1/g;
264              
265 28         234 $dbh->table_info($catalog, $schema, $like_expr)->fetchrow_array;
266 28         1007 }),
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 28     28   58 my $self = shift;
291 28         477 my $vars = $self->_vars;
292              
293 28         197 my $steps = 6 + scalar keys %{ $self->coderef_hooks };
  28         138  
294              
295 28   33     468 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 28         54843 $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   66 my $self = shift;
424 27         140 my $rsrc = $self->rsrc;
425 27         106 my $dbh = $self->dbh;
426 27         967890 my $vars = $self->_vars;
427              
428 27         723 my $copy_opts = $self->copy_opts;
429 27         716 my $helper = $self->_helper;
430              
431 27         278 my $catalog = $vars->{catalog};
432 27         81 my $schema = $vars->{schema};
433 27         501 my $orig_table_name = $self->table_name;
434 27         806 my $new_table_name = $self->new_table_name;
435              
436 27         575 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
437 27         2278 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
438              
439             # Sane defaults for timing
440 27   100     697 $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     172 $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   28 $dbh = $_;
448 13         93 my @ids = $dbh->primary_key($catalog, $schema, $orig_table_name);
449              
450 13 50       21728 die "No primary key found for $orig_table_name" unless @ids;
451 13 50       51 warn "Using the first column of a multi-column primary key for $orig_table_name" if @ids > 1;
452              
453 13         66 $ids[0];
454 27   66     407 });
455              
456 27         277 my $id_name_quote = $dbh->quote_identifier($id_name);
457              
458 27 50       792 if ($rsrc) {
459 27   66     172 $copy_opts->{dbic_storage} //= $rsrc->storage;
460 27   66     961 $copy_opts->{rsc} //= $rsrc->resultset->get_column($id_name);
461              
462 27   100     18974 $copy_opts->{dbic_retry_opts} //= {};
463 27   66     542 $copy_opts->{dbic_retry_opts}{max_attempts} //= $DEFAULT_MAX_ATTEMPTS;
464 27     3   627 $copy_opts->{dbic_retry_opts}{retry_handler} = sub { $self->_retry_handler(@_) };
  3         34568  
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         127 my @column_list = $self->_column_list;
473 27         73 my $column_list_str = join(', ', map { $dbh->quote_identifier($_) } @column_list );
  94         2044  
474              
475             # The INSERT..SELECT is a bit different depending on the RDBMS used, mostly because
476             # of the IGNORE part
477 27         878 my $insert_select_stmt = $helper->insert_select_stmt($column_list_str);
478              
479 27   66     252 $copy_opts->{count_stmt} //= "SELECT COUNT(*) FROM $orig_table_name_quote WHERE $id_name_quote BETWEEN ? AND ?";
480 27   66     155 $copy_opts->{stmt} //= $insert_select_stmt;
481              
482 27 50 66     589 $copy_opts->{progress_name} //= "Copying $orig_table_name" unless $copy_opts->{progress_bar};
483              
484 27         156 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 28     28   447 my $self = shift;
575              
576 28         125 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 28   33     1126560 my $dbms_name = $self->_vars->{dbms_name} = $dbh->get_info( $GetInfoType{SQL_DBMS_NAME} ) // $dbh->{Driver}->{Name};
581              
582 28         1141 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 28 50       186 die "OnlineDDL is not designed for $dbms_name systems yet!" unless Class::Load::load_optional_class($helper_class);
586              
587 28         3829 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 28     28 0 1581 my $self = shift;
616 28         108 my $rsrc = $self->rsrc;
617              
618 28         101 my $dbh = $self->dbh;
619 28         47431 my $helper = $self->_helper;
620              
621             # Get the current catalog/schema
622 28         10716 my ($catalog, $schema) = $helper->current_catalog_schema;
623              
624 28         739 $self->_vars->{catalog} = $catalog;
625 28         582 $self->_vars->{schema} = $schema;
626              
627             # Add in the post-connection details
628 28         470 my @stmts = $helper->post_connection_stmts;
629              
630 28 50       113 if ($rsrc) {
631             ### DBIC Storage
632              
633 28         83 my @post_connection_details = map { [ do_sql => $_ ] } @stmts;
  84         205  
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 28         168 my $storage = $rsrc->storage;
641 28         1848 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 28   33     117 my $ref = defined $on_connect_call && ref $on_connect_call;
645 28 0       83 unless ($on_connect_call) {
    0          
    0          
    50          
646 28         66 $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 28         85 $storage->_dbic_connect_attributes->{on_connect_call} = $on_connect_call;
680 28         119 $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 28         249 $dbh->{AutoCommit} = 1;
732 28         190 $dbh->{RaiseError} = 1;
733 28         302 $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 28     28 1 4618 my $self = shift;
784 28         764 my $reversible = $self->reversible;
785              
786 28         9580 $self->_progress_bar_setup;
787              
788             $reversible->run_reversibly(set_subname '_execute_part_one', sub {
789 28     28   5216 $self->create_new_table;
790 28         3504 $self->create_triggers;
791 27         4097 $self->copy_rows;
792 27         4001 $self->swap_tables;
793 28         452 });
794             $reversible->run_reversibly(set_subname '_execute_part_two', sub {
795 27     27   5587 $self->drop_old_table;
796 27         2299 $self->cleanup_foreign_keys;
797 27         5225 });
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 55     55 1 234 my ($self, $hook_name) = @_;
812              
813 55         252 my $hooks = $self->coderef_hooks;
814 55         853 my $vars = $self->_vars;
815              
816 55         341 my $progress = $vars->{progress_bar};
817              
818 55 100 66     441 return unless $hooks && $hooks->{$hook_name};
819              
820 15         135 $progress->message("Firing hook for $hook_name");
821              
822             # Fire the hook
823 15         679 $hooks->{$hook_name}->($self);
824              
825 15         219144 $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 651     651   7393 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 651     651   4704 my $dbh = $self->dbh_runner( run => sub { $_[0] } );
  651         2164  
846 651         2968 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 3     3   8 my ($self, $runner) = @_;
867 3         1123 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 3         95 my $error = $runner->last_exception;
873 3         173 my $is_retryable = $self->_helper->is_error_retryable($error);
874              
875 3 50       109 if ($is_retryable) {
876 3         55 my ($failed, $max) = ($runner->failed_attempt_count, $runner->max_attempts);
877 3         27 my $progress = $vars->{progress_bar};
878              
879             # Warn about the last error
880 3 50       17 $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 3         3006816 sleep $failed;
884              
885             # If retries are escalating, try forcing a disconnect
886 3 50       41 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 3 50       60 $progress->message( sprintf(
897             "Attempt %u of %u", $failed, $max
898             ) ) if $progress;
899             }
900              
901 3         235 return $is_retryable;
902             }
903              
904             sub dbh_runner {
905 1209     1209 1 8095 my ($self, $method, $coderef) = @_;
906 1209         2537 my $wantarray = wantarray;
907              
908 1209 50       7810 die "Only 'txn' or 'run' are acceptable run methods" unless $method =~ /^(?:txn|run)$/;
909              
910 1209         2138 my @res;
911 1209 50       6337 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 1209         11662 %{ $self->dbic_retry_opts },
920              
921 0     0   0 retry_handler => sub { $self->_retry_handler(@_) },
922 1209 100       2030 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 1209     1209   189631 my ($s, $c) = @_;
930 1209         5179 my $dbh = $s->rsrc->storage->dbh;
931              
932 1209         569133 local $_ = $dbh;
933 1209         3822 $c->($dbh); # also pass it in, because that's what DBIx::Connector does
934 1209         146917 };
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 1209         2574 my $br_method = 'run';
940 1209         25299 while ($block_runner->failed_attempt_count < $block_runner->max_attempts) {
941 1209         14753 eval {
942 1209 100       3613 unless (defined $wantarray) { $block_runner->$br_method($wrapper, $self, $coderef) }
  322 100       2075  
943 0         0 elsif ($wantarray) { @res = $block_runner->$br_method($wrapper, $self, $coderef) }
  167         932  
944 720         3583 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 1209         3157220 $br_method = '_run';
950              
951 1209 50       6736 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 1209         10865 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 1209 100       6426 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 180     180 1 521359 my ($self, @commands) = @_;
996              
997 180         496 foreach my $command (@commands) {
998 184         439 my $ref = ref $command;
999 184 50 33     1041 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 184 50   184   1185 $_->do( $ref ? @$command : $command );
1003 184         2185 });
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 28     28 1 80 my $self = shift;
1022 28         172 my $dbh = $self->dbh;
1023 28         1131534 my $vars = $self->_vars;
1024              
1025 28         251 my $progress = $vars->{progress_bar};
1026 28         729 my $reversible = $self->reversible;
1027 28         733 my $helper = $self->_helper;
1028              
1029 28         1079 my $orig_table_name = $self->table_name;
1030 28         860 my $new_table_name = $self->new_table_name;
1031              
1032 28         885 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
1033 28         1502 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
1034              
1035             # ANSI quotes could also appear in the statement
1036 28         556 my $orig_table_name_ansi_quote = '"'.$orig_table_name.'"';
1037              
1038 28         237 $progress->message("Creating new table $new_table_name");
1039              
1040 28         1898 my $table_sql = $helper->create_table_sql($orig_table_name);
1041 28 50       116 die "Table $orig_table_name does not exist in the database!" unless $table_sql;
1042              
1043 28 50       126 $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 28         177 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 28         997 $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 28         148 $self->dbh_runner_do($table_sql);
1065              
1066             # Undo commands, including a failure warning update
1067 28         1130 $reversible->failure_warning("\nDropping the new table and rolling back to start!\n\n");
1068 28     1   1964 $reversible->add_undo(sub { $self->dbh_runner_do("DROP TABLE $new_table_name_quote") });
  1         451  
1069              
1070 28         2125 $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 28     28 1 62 my $self = shift;
1082 28         132 my $rsrc = $self->rsrc;
1083 28         204 my $dbh = $self->dbh;
1084 28         987186 my $vars = $self->_vars;
1085              
1086 28         264 my $progress = $vars->{progress_bar};
1087 28         646 my $reversible = $self->reversible;
1088 28         1253 my $helper = $self->_helper;
1089              
1090 28         322 my $catalog = $vars->{catalog};
1091 28         86 my $schema = $vars->{schema};
1092 28         507 my $orig_table_name = $self->table_name;
1093 28         631 my $new_table_name = $self->new_table_name;
1094              
1095             # Fire the before_triggers hook, which would typically include the DDL
1096 28         348 $self->fire_hook('before_triggers');
1097              
1098 28         1242 $progress->message("Creating triggers");
1099              
1100             # This shouldn't be cached until now, since the actual DDL may change the column list
1101 28         1329 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 28         69 my @unique_ids;
1108 28         147 my $indexes = $self->_get_idx_hash($orig_table_name);
1109              
1110 28         72 my %potential_unique_ids;
1111 28         103 $potential_unique_ids{ $_->{name} } = $_ for grep { $_->{unique} } values %$indexes;
  90         295  
1112              
1113 28         75 my %column_set = map { $_ => 1 } @column_list;
  100         295  
1114 28         96 foreach my $index_name ('PRIMARY',
1115             # sort by the number of columns (asc), though PRIMARY still has top priority
1116 16         26 sort { scalar(@{ $potential_unique_ids{$a}{columns} }) <=> scalar(@{ $potential_unique_ids{$b}{columns} }) }
  16         32  
  16         42  
1117 64         202 grep { $_ ne 'PRIMARY' }
1118             keys %potential_unique_ids
1119             ) {
1120 64         98 my @unique_cols = @{ $potential_unique_ids{$index_name}{columns} };
  64         171  
1121 64 50       142 next unless @unique_cols;
1122              
1123             # Only use this set if all of the columns exist in both tables
1124 64 50   102   288 next unless all { $column_set{$_} } @unique_cols;
  102         193  
1125              
1126 64         358 @unique_ids = @unique_cols;
1127             }
1128              
1129 28 50       90 die "Cannot find an appropriate unique index for $orig_table_name!" unless @unique_ids;
1130              
1131             ### Check to make sure existing triggers aren't on the table
1132              
1133 28 100       176 die "Found conflicting triggers on $orig_table_name! Please remove them first, so that our INSERT/UPDATE/DELETE triggers can be applied."
1134             if $helper->has_conflicting_triggers_on_table($orig_table_name);
1135              
1136             ### Find a good set of trigger names
1137              
1138 27         123 foreach my $trigger_type (qw< INSERT UPDATE DELETE >) {
1139 81         2399 my $trigger_name = $helper->find_new_trigger_identifier(
1140             "${orig_table_name}_onlineddl_".lc($trigger_type)
1141             );
1142 81         552 $vars->{trigger_names} {$trigger_type} = $trigger_name;
1143 81         698 $vars->{trigger_names_quoted}{$trigger_type} = $dbh->quote_identifier($trigger_name);
1144             }
1145              
1146             ### Now create the triggers, with (mostly) ANSI SQL
1147              
1148 27         1040 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
1149 27         552 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
1150              
1151 27         920 my $column_list_str = join(', ', map { $dbh->quote_identifier($_) } @column_list );
  94         1558  
1152 27         584 my $new_column_list_str = join(', ', map { "NEW.".$dbh->quote_identifier($_) } @column_list );
  94         1368  
1153              
1154 27         907 my $nseo = $helper->null_safe_equals_op;
1155 27         56 my %trigger_dml_stmts;
1156              
1157             # Using REPLACE just in case the row already exists from the copy
1158 27         170 $trigger_dml_stmts{replace} = join("\n",
1159             "REPLACE INTO $new_table_name_quote",
1160             " ($column_list_str)",
1161             "VALUES",
1162             " ($new_column_list_str)",
1163             );
1164              
1165             my $update_unique_where_str = join(' AND ',
1166             (map {
1167 27         85 join(
  46         976  
1168             # Use NULL-safe equals, since unique indexes could be nullable
1169             " $nseo ",
1170             "OLD.".$dbh->quote_identifier($_),
1171             "NEW.".$dbh->quote_identifier($_),
1172             );
1173             } @unique_ids)
1174             );
1175              
1176             my $delete_unique_where_str = join(' AND ',
1177             (map {
1178 27         1079 join(
  46         829  
1179             # Use NULL-safe equals, since unique indexes could be nullable
1180             " $nseo ",
1181             "$new_table_name_quote.".$dbh->quote_identifier($_),
1182             "OLD.".$dbh->quote_identifier($_),
1183             );
1184             } @unique_ids)
1185             );
1186              
1187             # For the UPDATE trigger, DELETE the row, but only if the unique IDs have been
1188             # changed. The "NOT ($update_unique_where_str)" part keeps from deleting rows where
1189             # the unique ID is untouched.
1190 27         1100 $trigger_dml_stmts{delete_for_update} = join("\n",
1191             "DELETE FROM $new_table_name_quote WHERE",
1192             " NOT ($update_unique_where_str) AND",
1193             " $delete_unique_where_str"
1194             );
1195              
1196 27         104 $trigger_dml_stmts{delete_for_delete} = join("\n",
1197             "DELETE FROM $new_table_name_quote WHERE",
1198             " $delete_unique_where_str"
1199             );
1200              
1201 27         162 $helper->modify_trigger_dml_stmts( \%trigger_dml_stmts );
1202              
1203 27         75 foreach my $trigger_type (qw< INSERT UPDATE DELETE >) {
1204             my $trigger_header = join(' ',
1205 81         4476 "CREATE TRIGGER ".$vars->{trigger_names_quoted}{$trigger_type},
1206             "AFTER $trigger_type ON $orig_table_name_quote FOR EACH ROW"
1207             );
1208              
1209             # Even though some of these are just a single SQL statement, not every RDBMS
1210             # (like SQLite) supports leaving out the BEGIN/END keywords.
1211 81         275 my $trigger_sql = join("\n",
1212             $trigger_header,
1213             "BEGIN",
1214             '',
1215             );
1216              
1217 81 100       431 if ($trigger_type eq 'INSERT') {
    100          
    50          
1218             # INSERT trigger: Just a REPLACE command
1219 27         86 $trigger_sql .= $trigger_dml_stmts{replace}.';';
1220             }
1221             elsif ($trigger_type eq 'UPDATE') {
1222             # UPDATE trigger: DELETE special unique ID changes, then another REPLACE command.
1223             $trigger_sql .= join("\n",
1224             $trigger_dml_stmts{delete_for_update}.';',
1225 27         142 $trigger_dml_stmts{replace}.';',
1226             );
1227             }
1228             elsif ($trigger_type eq 'DELETE') {
1229             # DELETE trigger: Just a DELETE command
1230 27         130 $trigger_sql .= $trigger_dml_stmts{delete_for_delete}.';';
1231             }
1232 81         178 $trigger_sql .= "\nEND";
1233              
1234             # DOIT!
1235 81         384 $self->dbh_runner_do($trigger_sql);
1236              
1237             $reversible->add_undo(sub {
1238 0     0   0 $self->dbh_runner_do( "DROP TRIGGER IF EXISTS ".$self->_vars->{trigger_names_quoted}{$trigger_type} );
1239 81         3786 });
1240             }
1241              
1242 27         2003 $progress->update;
1243             }
1244              
1245             #pod =head2 copy_rows
1246             #pod
1247             #pod Fires up a L process to copy all of the rows from the old table to
1248             #pod the new.
1249             #pod
1250             #pod =cut
1251              
1252             sub copy_rows {
1253 27     27 1 66 my $self = shift;
1254 27         162 my $dbh = $self->dbh;
1255 27         954432 my $vars = $self->_vars;
1256              
1257 27         266 my $progress = $vars->{progress_bar};
1258 27         135 my $copy_opts = $self->_fill_copy_opts;
1259              
1260 27         260 $progress->message("Copying all rows to the new table");
1261              
1262 27         1594 DBIx::BatchChunker->construct_and_execute( %$copy_opts );
1263 27         73311518 $vars->{new_table_copied} = 1;
1264              
1265             # Analyze the table, since we have a ton of new rows now
1266 27         142 $progress->message("Analyzing table");
1267 27         1534 $self->_helper->analyze_table( $self->new_table_name );
1268              
1269 27         280 $progress->update;
1270             }
1271              
1272             #pod =head2 swap_tables
1273             #pod
1274             #pod With the new table completely modified and set up, this swaps the old/new tables.
1275             #pod
1276             #pod =cut
1277              
1278             sub swap_tables {
1279 27     27 1 68 my $self = shift;
1280 27         127 my $dbh = $self->dbh;
1281 27         975110 my $vars = $self->_vars;
1282              
1283 27         255 my $progress = $vars->{progress_bar};
1284 27         627 my $reversible = $self->reversible;
1285 27         599 my $helper = $self->_helper;
1286              
1287 27         255 my $catalog = $vars->{catalog};
1288 27         84 my $schema = $vars->{schema};
1289 27         438 my $orig_table_name = $self->table_name;
1290 27         565 my $new_table_name = $self->new_table_name;
1291              
1292 27   50     392 my $escape = $dbh->get_info( $GetInfoType{SQL_SEARCH_PATTERN_ESCAPE} ) // '\\';
1293              
1294             # Fire the before_swap hook
1295 27         563 $self->fire_hook('before_swap');
1296              
1297 27 50 33     249 if ($helper->dbms_uses_global_fk_namespace || $helper->child_fks_need_adjusting) {
1298             # The existing parent/child FK list needs to be captured prior to the swap. The FKs
1299             # have already been created, and possibly changed/deleted, from the new table, so we
1300             # use that as reference. They have *not* been re-created on the child tables, so
1301             # the original table is used as reference.
1302 0   0     0 my $fk_hash = $vars->{foreign_keys}{definitions} //= {};
1303             $self->dbh_runner(run => set_subname '_fk_parent_info_query', sub {
1304 0     0   0 $fk_hash->{parent} = $self->_fk_info_to_hash( $helper->foreign_key_info(undef, undef, undef, $catalog, $schema, $new_table_name) );
1305 0         0 });
1306             $self->dbh_runner(run => set_subname '_fk_child_info_query', sub {
1307 0     0   0 $fk_hash->{child} = $self->_fk_info_to_hash( $helper->foreign_key_info($catalog, $schema, $orig_table_name, undef, undef, undef) );
1308 0         0 });
1309              
1310             # Furthermore, we should capture the indexes from parent/child tables in case the data
1311             # is needed for FK cleanup
1312 0   0     0 my $idx_hash = $vars->{indexes}{definitions} //= {};
1313 0 0 0     0 if ($dbh->can('statistics_info') && %$fk_hash) {
1314 0         0 foreach my $fk_table_name (
1315             uniq sort
1316 0 0 0     0 grep { defined && $_ ne $orig_table_name && $_ ne $new_table_name }
1317 0         0 map { ($_->{pk_table_name}, $_->{fk_table_name}) }
1318 0         0 (values %{$fk_hash->{parent}}, values %{$fk_hash->{child}})
  0         0  
1319             ) {
1320 0         0 $idx_hash->{$fk_table_name} = $self->_get_idx_hash($fk_table_name);
1321             }
1322             }
1323             }
1324              
1325             # Find an "_old" table name first
1326             my $old_table_name = $vars->{old_table_name} = $self->_find_new_identifier(
1327             "_${orig_table_name}_old" => set_subname('_old_table_name_finder', sub {
1328 27     27   88 my ($d, $like_expr) = @_;
1329 27         320 $like_expr =~ s/([_%])/$escape$1/g;
1330              
1331 27         232 $d->table_info($catalog, $schema, $like_expr)->fetchrow_array;
1332 27         721 }),
1333             'SQL_MAXIMUM_TABLE_NAME_LENGTH',
1334             );
1335 27         280 my $old_table_name_quote = $dbh->quote_identifier($old_table_name);
1336              
1337 27         1432 $progress->message("Swapping tables ($new_table_name --> $orig_table_name --> $old_table_name)");
1338              
1339             # Let's swap tables!
1340 27         1640 $helper->swap_tables($new_table_name, $orig_table_name, $old_table_name);
1341              
1342             # Kill the undo stack now, just in case something weird happens between now and the
1343             # end of the reversibly block. We've reached a "mostly successful" state, so rolling
1344             # back here would be undesirable.
1345 27         1535 $reversible->clear_undo;
1346 27         2103 $vars->{new_table_swapped} = 1;
1347              
1348 27         246 $progress->update;
1349             }
1350              
1351             #pod =head2 drop_old_table
1352             #pod
1353             #pod Drops the old table. This will also remove old foreign keys on child tables. (Those FKs
1354             #pod are re-applied to the new table in the next step.)
1355             #pod
1356             #pod =cut
1357              
1358             sub drop_old_table {
1359 27     27 1 78 my $self = shift;
1360 27         159 my $dbh = $self->dbh;
1361 27         969692 my $vars = $self->_vars;
1362              
1363 27         263 my $progress = $vars->{progress_bar};
1364 27         639 my $reversible = $self->reversible;
1365 27         856 my $helper = $self->_helper;
1366              
1367 27         369 my $old_table_name = $vars->{old_table_name};
1368 27         293 my $old_table_name_quote = $dbh->quote_identifier($old_table_name);
1369              
1370 27         1597 $reversible->failure_warning( join "\n",
1371             '',
1372             "The new table has been swapped, but since the process was interrupted, foreign keys will",
1373             "need to be cleaned up, and the old table dropped.",
1374             '',
1375             );
1376              
1377             # The RDBMS may complain about dangling non-functional FKs if the DROP happens first,
1378             # so let's remove those child FKs first, and reapply them later. We turn off FK
1379             # checks, so these constraint drops are quick and low risk.
1380             #
1381             # SQLite doesn't actually support DROP CONSTRAINT, but it doesn't do any messy business with
1382             # FK renames, either. So, SQLite can just skip this step.
1383 27 50       1236 if ($helper->child_fks_need_adjusting) {
1384 0         0 $progress->message("Removing FKs from child tables");
1385              
1386 0         0 $self->dbh_runner_do(
1387             $helper->remove_fks_from_child_tables_stmts
1388             );
1389             }
1390              
1391             # Now, the actual DROP
1392 27         245 $progress->message("Dropping old table $old_table_name");
1393              
1394 27         1202 $self->dbh_runner_do("DROP TABLE $old_table_name_quote");
1395              
1396 27         217 $progress->update;
1397             }
1398              
1399             #pod =head2 cleanup_foreign_keys
1400             #pod
1401             #pod Clean up foreign keys on both the new and child tables.
1402             #pod
1403             #pod =cut
1404              
1405             sub cleanup_foreign_keys {
1406 27     27 1 68 my $self = shift;
1407 27         152 my $dbh = $self->dbh;
1408 27         925805 my $vars = $self->_vars;
1409              
1410 27         306 my $progress = $vars->{progress_bar};
1411 27         629 my $reversible = $self->reversible;
1412 27         777 my $helper = $self->_helper;
1413              
1414 27         685 $reversible->failure_warning( join "\n",
1415             '',
1416             "The new table is live, but since the process was interrupted, foreign keys will need to be",
1417             "cleaned up.",
1418             '',
1419             );
1420              
1421 27 50       1094 if ($helper->dbms_uses_global_fk_namespace) {
1422             # The DB has global namespaces for foreign keys, so we are renaming them back to
1423             # their original names. The original table has already been dropped, so there's
1424             # no more risk of bumping into that namespace.
1425 0         0 $progress->message("Renaming parent FKs back to the original constraint names");
1426              
1427 0         0 $self->dbh_runner_do(
1428             $helper->rename_fks_back_to_original_stmts
1429             );
1430             }
1431              
1432 27 50       147 if ($helper->child_fks_need_adjusting) {
1433             # Since we captured the child FK names prior to the swap, they should have the
1434             # original FK names, even before MySQL's "helpful" changes on "${tbl_name}_ibfk_" FK
1435             # names.
1436 0         0 $progress->message("Adding FKs back on child tables");
1437              
1438 0         0 $self->dbh_runner_do(
1439             $helper->add_fks_back_to_child_tables_stmts
1440             );
1441              
1442             # The RDBMS may need some post-FK cleanup
1443 0         0 $progress->message("Post-FK cleanup");
1444              
1445 0         0 $self->dbh_runner_do(
1446             $helper->post_fk_add_cleanup_stmts
1447             );
1448             }
1449              
1450 27         247 $progress->update;
1451             }
1452              
1453             ### Private methods
1454              
1455             sub _find_new_identifier {
1456 139     139   2882 my ($self, $desired_identifier, $finder_sub, $length_info_str) = @_;
1457 139   100     742 $length_info_str ||= 'SQL_MAXIMUM_IDENTIFIER_LENGTH';
1458              
1459 139         288 state $hash_digits = ['a' .. 'z', '0' .. '9'];
1460              
1461 139         427 my $hash = join '', map { $hash_digits->[rand @$hash_digits] } 1 .. 10;
  1390         3539  
1462              
1463             # Test out some potential names
1464 139         883 my @potential_names = (
1465             $desired_identifier, "_${desired_identifier}",
1466             "${desired_identifier}_${hash}", "_${desired_identifier}_${hash}",
1467             $hash, "_${hash}"
1468             );
1469              
1470 139   50     650 my $max_len = $self->dbh->get_info( $GetInfoType{$length_info_str} ) || 256;
1471              
1472 139         5092476 my $new_name;
1473 139         496 foreach my $potential_name (@potential_names) {
1474 139         868 $potential_name = substr($potential_name, 0, $max_len); # avoid the ID name character limit
1475              
1476             my @results = $self->dbh_runner(run => set_subname '_find_new_identifier_dbh_runner', sub {
1477 139     139   524 $finder_sub->($_, $potential_name);
1478 139         2224 });
1479              
1480             # Skip if we found it
1481 139 50       731 next if @results;
1482              
1483 139         401 $new_name = $potential_name;
1484 139         326 last;
1485             }
1486              
1487             # This really shouldn't happen...
1488 139 50       475 die "Cannot find a proper identifier name for $desired_identifier! All of them are taken!" unless defined $new_name;
1489              
1490 139         1623 return $new_name;
1491             }
1492              
1493             sub _column_list {
1494 55     55   119 my $self = shift;
1495 55         275 my $dbh = $self->dbh;
1496 55         2105663 my $vars = $self->_vars;
1497              
1498 55         506 my $catalog = $vars->{catalog};
1499 55         199 my $schema = $vars->{schema};
1500 55         1019 my $orig_table_name = $self->table_name;
1501 55         1628 my $new_table_name = $self->new_table_name;
1502              
1503 55         575 my (@old_column_list, @new_column_list);
1504             $self->dbh_runner(run => set_subname '_column_list_runner', sub {
1505 55     55   130 $dbh = $_;
1506             @old_column_list =
1507 194         87913 map { $_->{COLUMN_NAME} }
1508 55         114 @{ $dbh->column_info( $catalog, $schema, $orig_table_name, '%' )->fetchall_arrayref({ COLUMN_NAME => 1 }) }
  55         400  
1509             ;
1510             @new_column_list =
1511 222         67542 map { $_->{COLUMN_NAME} }
1512 55         311 @{ $dbh->column_info( $catalog, $schema, $new_table_name, '%' )->fetchall_arrayref({ COLUMN_NAME => 1 }) }
  55         402  
1513             ;
1514 55         872 });
1515              
1516             # We only care about columns that exist in both tables. If a column was added on the
1517             # new table, there's no data to copy. If a column was deleted from the new table, we
1518             # don't care about keeping it.
1519 55         354 my %new_column_set = map { $_ => 1 } @new_column_list;
  222         669  
1520 55         158 return grep { $new_column_set{$_} } @old_column_list;
  194         797  
1521             }
1522              
1523             sub _get_idx_hash {
1524 28     28   108 my ($self, $table_name) = @_;
1525              
1526 28         802 my $vars = $self->_vars;
1527 28         315 my $catalog = $vars->{catalog};
1528 28         77 my $schema = $vars->{schema};
1529              
1530             my %idxs = (
1531             PRIMARY => {
1532             name => 'PRIMARY',
1533             columns => [ $self->dbh_runner(run => set_subname '_pk_info_query', sub {
1534 28     28   232 $_->primary_key($catalog, $schema, $table_name)
1535 28         329 }) ],
1536             unique => 1,
1537             },
1538             );
1539 28 50       113 delete $idxs{PRIMARY} unless @{ $idxs{PRIMARY}{columns} };
  28         126  
1540              
1541 28 50       131 return \%idxs unless $self->dbh->can('statistics_info');
1542              
1543             # Sometimes, this still dies, even with the 'can' check (eg: older DBD::mysql drivers)
1544 28         1119489 my $index_stats = [];
1545 28         77 eval {
1546             $index_stats = $self->dbh_runner(run => set_subname '_idx_info_query', sub {
1547 28     28   253 $_->statistics_info($catalog, $schema, $table_name, 0, 1)->fetchall_arrayref({});
1548 28         391 });
1549             };
1550 28 50       167 $index_stats = [] if $@;
1551              
1552 28         111 foreach my $index_name (uniq map { $_->{INDEX_NAME} } @$index_stats) {
  96         287  
1553 62     186   281 my $index_stat = first { $_->{INDEX_NAME} eq $index_name } @$index_stats;
  186         264  
1554             my @cols =
1555 96         197 map { $_->{COLUMN_NAME} }
1556 38         96 sort { $a->{ORDINAL_POSITION} <=> $b->{ORDINAL_POSITION} }
1557 62         244 grep { $_->{INDEX_NAME} eq $index_name }
  314         553  
1558             @$index_stats
1559             ;
1560             $idxs{$index_name} = {
1561             name => $index_name,
1562             columns => \@cols,
1563             unique => !$index_stat->{NON_UNIQUE},
1564 62         275 };
1565             }
1566              
1567 28         854 return \%idxs;
1568             }
1569              
1570             sub _fk_info_to_hash {
1571 0     0     my ($self, $fk_sth) = @_;
1572 0           my $vars = $self->_vars;
1573 0           my $dbh = $self->dbh;
1574              
1575             # NOTE: Need to account for alternate ODBC names
1576              
1577 0           my @fk_rows = @{ $fk_sth->fetchall_arrayref({}) };
  0            
1578             @fk_rows = sort {
1579             # Sort by FK name, then by the column sequence number
1580 0           $a->{FK_NAME} cmp $b->{FK_NAME} ||
1581             ($a->{KEY_SEQ} // $a->{ORDINAL_POSITION}) <=> ($a->{KEY_SEQ} // $a->{ORDINAL_POSITION})
1582 0 0 0       } @fk_rows;
      0        
1583              
1584 0           my (%fks, %create_table_sql);
1585 0           foreach my $row (@fk_rows) {
1586             # Some of these rows aren't even FKs
1587 0 0 0       next unless $row->{PKTABLE_NAME} || $row->{UK_TABLE_CAT};
1588 0 0 0       next unless $row->{FKTABLE_NAME} || $row->{FK_TABLE_NAME};
1589              
1590 0   0       my $fk_name = $row->{FK_NAME} // $row->{FKCOLUMN_NAME};
1591 0   0       my $fk_table_name = $row->{FKTABLE_NAME} // $row->{FK_TABLE_NAME};
1592              
1593             my $key = join( '.',
1594             $row->{PKTABLE_NAME} // $row->{UK_TABLE_CAT},
1595 0   0       $fk_name,
1596             );
1597              
1598             # Since there may be multiple columns per FK, those associated columns are
1599             # arrayrefs.
1600 0 0         unless ($fks{$key}) {
1601              
1602             $fks{$key} = {
1603             fk_name => $fk_name,
1604              
1605             # The table where the original PK exists
1606             pk_table_name => $row->{PKTABLE_NAME} // $row->{UK_TABLE_CAT},
1607             pk_columns => [ $row->{PKCOLUMN_NAME} // $row->{UK_COLUMN_NAME} ],
1608              
1609             # The table where the FK constraint has been declared
1610             fk_table_name => $fk_table_name,
1611 0   0       fk_columns => [ $row->{FKCOLUMN_NAME} // $row->{FK_COLUMN_NAME} ],
      0        
      0        
1612             };
1613              
1614             # Sadly, foreign_key_info doesn't always fill in all of the details for the FK, so the
1615             # CREATE TABLE SQL is actually the better record. Fortunately, this is all ANSI SQL.
1616 0   0       my $create_table_sql = $create_table_sql{$fk_table_name} //= $self->_helper->create_table_sql($fk_table_name);
1617 0           my $fk_name_quote_re = '(?:'.join('|',
1618             quotemeta( $dbh->quote_identifier($fk_name) ), quotemeta('"'.$fk_name.'"'), quotemeta($fk_name)
1619             ).')';
1620              
1621 0 0         if ($create_table_sql =~ m<
1622             CONSTRAINT \s $fk_name_quote_re \s ( # start capture of full SQL
1623             FOREIGN \s KEY \s \( [^\)]+ \) \s # "FOREIGN KEY" plus column list (which we already have above)
1624             REFERENCES \s [^\(]+ \s \( [^\)]+ \) # "REFERENCES" plus table+column list (again, already captured above)
1625             \s? ( [^\)\,]* ) # ON DELETE/UPDATE, DEFER, MATCH, etc.
1626             ) # end capture of full SQL
1627             >isx) {
1628 0           my ($fk_sql, $extra_sql) = ($1, $2);
1629 0           $fk_sql =~ s/^\s+|\s+$//g;
1630              
1631 0           $fks{$key}{fk_sql} = $fk_sql;
1632 0 0         $fks{$key}{delete_rule} = $1 if $extra_sql =~ /ON DELETE ((?:SET |NO )?\w+)/i;
1633 0 0         $fks{$key}{update_rule} = $1 if $extra_sql =~ /ON UPDATE ((?:SET |NO )?\w+)/i;
1634 0 0         $fks{$key}{defer} = $1 if $extra_sql =~ /((?:NOT )?DEFERRABLE(?: INITIALLY \w+)?)/i;
1635 0 0         $fks{$key}{match} = $1 if $extra_sql =~ /(MATCH \w+)/i;
1636             }
1637             }
1638             else {
1639 0   0       push @{ $fks{$key}{pk_columns} }, $row->{PKCOLUMN_NAME} // $row->{UK_COLUMN_NAME};
  0            
1640 0   0       push @{ $fks{$key}{fk_columns} }, $row->{FKCOLUMN_NAME} // $row->{FK_COLUMN_NAME};
  0            
1641             }
1642             }
1643              
1644 0           return \%fks;
1645             }
1646              
1647             sub _fk_to_sql {
1648 0     0     my ($self, $fk) = @_;
1649 0           my $dbh = $self->dbh;
1650              
1651             # Everything after the CONSTRAINT keyword (ANSI SQL)
1652              
1653 0 0         if ($fk->{fk_sql}) {
1654             # Already have most of the SQL
1655             return join(' ',
1656             $dbh->quote_identifier($fk->{fk_name}),
1657             $fk->{fk_sql},
1658 0           );
1659             }
1660              
1661             return join(' ',
1662             $dbh->quote_identifier($fk->{fk_name}),
1663             'FOREIGN KEY',
1664 0           '('.join(', ', map { $dbh->quote_identifier($_) } @{ $fk->{fk_columns} }).')',
  0            
1665             'REFERENCES',
1666             $dbh->quote_identifier($fk->{pk_table_name}),
1667 0           '('.join(', ', map { $dbh->quote_identifier($_) } @{ $fk->{pk_columns} }).')',
  0            
1668             ( $fk->{match} ? $fk->{match} : () ),
1669             ( $fk->{delete_rule} ? 'ON DELETE '.$fk->{delete_rule} : () ),
1670             ( $fk->{update_rule} ? 'ON UPDATE '.$fk->{update_rule} : () ),
1671 0 0         ( $fk->{defer} ? $fk->{defer} : () ),
    0          
    0          
    0          
1672             );
1673             }
1674              
1675             #pod =head1 SEE ALSO
1676             #pod
1677             #pod =over
1678             #pod
1679             #pod =item *
1680             #pod
1681             #pod L
1682             #pod
1683             #pod =item *
1684             #pod
1685             #pod L
1686             #pod
1687             #pod =item *
1688             #pod
1689             #pod L
1690             #pod
1691             #pod =item *
1692             #pod
1693             #pod L
1694             #pod
1695             #pod =back
1696             #pod
1697             #pod =head1 WHY YET ANOTHER OSC?
1698             #pod
1699             #pod The biggest reason is that none of the above fully support foreign key constraints.
1700             #pod Percona's C comes close, but also includes this paragraph:
1701             #pod
1702             #pod Due to a limitation in MySQL, foreign keys will not have the same names after the ALTER
1703             #pod that they did prior to it. The tool has to rename the foreign key when it redefines it,
1704             #pod which adds a leading underscore to the name. In some cases, MySQL also automatically
1705             #pod renames indexes required for the foreign key.
1706             #pod
1707             #pod So, tables swapped with C are not exactly what they used to be before the swap.
1708             #pod It also had a number of other quirks that just didn't work out for us, related to FKs and
1709             #pod the amount of switches required to make it (semi-)work.
1710             #pod
1711             #pod Additionally, by making DBIx::OnlineDDL its own Perl module, it's a lot easier to run
1712             #pod Perl-based schema changes along side L without having to switch
1713             #pod between Perl and CLI. If other people want to subclass this module for their own
1714             #pod environment-specific quirks, they have the power to do so, too.
1715             #pod
1716             #pod =cut
1717              
1718             1;
1719              
1720             __END__