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__ |