File Coverage

blib/lib/DBIx/OnlineDDL/Helper/Base.pm
Criterion Covered Total %
statement 40 114 35.0
branch 1 8 12.5
condition 1 12 8.3
subroutine 12 31 38.7
pod 20 22 90.9
total 74 187 39.5


line stmt bran cond sub pod time code
1             package DBIx::OnlineDDL::Helper::Base;
2              
3             our $AUTHORITY = 'cpan:GSG';
4             # ABSTRACT: Private OnlineDDL helper for RDBMS-specific code
5 2     2   903 use version;
  2         5  
  2         8  
6             our $VERSION = 'v1.0.0'; # VERSION
7              
8 2     2   122 use v5.10;
  2         6  
9 2     2   6 use Moo;
  2         4  
  2         9  
10              
11 2     2   466 use Types::Standard qw( InstanceOf );
  2         3  
  2         11  
12              
13 2     2   718 use DBI::Const::GetInfoType;
  2         5  
  2         199  
14 2     2   13 use Sub::Util qw( set_subname );
  2         83  
  2         80  
15 2     2   17 use version 0.77 ();
  2         28  
  2         59  
16              
17 2     2   11 use namespace::clean; # don't export the above
  2         3  
  2         8  
18              
19             #pod =encoding utf8
20             #pod
21             #pod =head1 DESCRIPTION
22             #pod
23             #pod This is a private helper module for any RDBMS-specific code. B
24             #pod methods or attributes here are subject to change.>
25             #pod
26             #pod You should really be reading documentation for L. The documentation
27             #pod here is mainly to benefit any developers who might want to create their own subclass
28             #pod module for their RDBMS and submit it to us. Or fix bugs with the existing helpers.
29             #pod
30             #pod =cut
31              
32             #pod =head1 PRIVATE ATTRIBUTES
33             #pod
34             #pod =head2 online_ddl
35             #pod
36             #pod Points back to the parent L. This comes with a bunch of handles to be
37             #pod able to call common methods with fewer keystrokes.
38             #pod
39             #pod =cut
40              
41             has online_ddl => (
42             is => 'ro',
43             isa => InstanceOf['DBIx::OnlineDDL'],
44             required => 1,
45             weak_ref => 1,
46             handles => {
47             vars => '_vars',
48             dbh => 'dbh',
49             table_name => 'table_name',
50             new_table_name => 'new_table_name',
51             copy_opts => 'copy_opts',
52             db_timeouts => 'db_timeouts',
53             dbh_runner => 'dbh_runner',
54             dbh_runner_do => 'dbh_runner_do',
55             find_new_identifier => '_find_new_identifier',
56             get_idx_hash => '_get_idx_hash',
57             fk_to_sql => '_fk_to_sql',
58             },
59             );
60              
61             # Other "handles"
62 0     0 0 0 sub dbms_name { shift->vars->{dbms_name} } # used for die errors only
63 0     0 0 0 sub progress { shift->vars->{progress_bar} }
64              
65             #pod =head1 PRIVATE CLASS "ATTRIBUTES"
66             #pod
67             #pod =head2 dbms_uses_global_fk_namespace
68             #pod
69             #pod If true, OnlineDDL will rename the FKs in the new table to make sure they don't conflict,
70             #pod and rename them back after the swap.
71             #pod
72             #pod =cut
73              
74 0     0 1 0 sub dbms_uses_global_fk_namespace { 0 }
75              
76             #pod =head2 child_fks_need_adjusting
77             #pod
78             #pod If true, OnlineDDL will call helper methods to adjust FKs bound to child tables.
79             #pod
80             #pod =cut
81              
82 0     0 1 0 sub child_fks_need_adjusting { 0 }
83              
84             #pod =head2 null_safe_equals_op
85             #pod
86             #pod This is the operator that the DB uses for NULL-safe equals comparisons. It would match
87             #pod this truth table:
88             #pod
89             #pod 0 0 --> TRUE
90             #pod 0 1 --> FALSE
91             #pod 0 NULL --> FALSE (instead of NULL)
92             #pod NULL NULL --> TRUE (instead of NULL)
93             #pod
94             #pod The ANSI SQL version is C, but others RDBMS typically use something
95             #pod less bulky.
96             #pod
97             #pod =cut
98              
99 0     0 1 0 sub null_safe_equals_op { 'IS NOT DISTINCT FROM' }
100              
101             #pod =head2 mmver
102             #pod
103             #pod The major/minor version of the currently connected server, converted to "numified" form
104             #pod via L, after parsing out the dotted notation (ie: C<5.7.33> instead of
105             #pod C<5.7.33-36-log>). This allows for version comparisons.
106             #pod
107             #pod =cut
108              
109             sub mmver {
110 30     30 1 620 my $self = shift;
111 30         423 my ($mmver) = ($self->dbh->get_info($GetInfoType{SQL_DBMS_VER}) =~ /(\d+\.\d+(?:\.\d+)?)/);
112 30         848305 return version->parse("v$mmver")->numify;
113             }
114              
115             #pod =head1 PRIVATE HELPER METHODS
116             #pod
117             #pod As the base module, all of these methods will use ANSI SQL, since there is no assumption
118             #pod of the type of RDBMS used yet. Some of these methods may just immediately die, as there
119             #pod may not be a (safe) standard way of doing that task.
120             #pod
121             #pod =head2 current_catalog_schema
122             #pod
123             #pod ($catalog, $schema) = $helper->current_catalog_schema;
124             #pod
125             #pod Figure out the currently-selected catalog and schema (database name) from the database.
126             #pod
127             #pod =cut
128              
129             sub current_catalog_schema {
130 0     0 1 0 my $self = shift;
131              
132             # Try to guess from the DSN parameters
133 0         0 my %dsn = map { /^(.+)=(.+)$/; lc($1) => $2; } (split /\;/, $self->dbh->{Name});
  0         0  
  0         0  
134 0         0 my $catalog = $dsn{catalog};
135 0   0     0 my $schema = $dsn{database} // $dsn{schema};
136              
137 0         0 return ($catalog, $schema);
138             }
139              
140             #pod =head2 insert_select_stmt
141             #pod
142             #pod $insert_select_stmt = $helper->insert_select_stmt($column_list_str);
143             #pod
144             #pod Return an C statement to copy rows from the old table to the new, in
145             #pod such a way that doesn't cause "duplicate row" errors. This is used by
146             #pod L for the copy operation, so it will need C
147             #pod placeholders.
148             #pod
149             #pod =cut
150              
151             sub insert_select_stmt {
152 0     0 1 0 my ($self, $column_list_str) = @_;
153              
154 0         0 my $dbh = $self->dbh;
155              
156 0         0 my $orig_table_name = $self->table_name;
157 0         0 my $new_table_name = $self->new_table_name;
158              
159 0         0 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
160 0         0 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
161              
162 0         0 my $id_name = $self->copy_opts->{id_name};
163 0         0 my $old_full_id_name_quote = $dbh->quote_identifier(undef, $orig_table_name, $id_name);
164 0         0 my $new_full_id_name_quote = $dbh->quote_identifier(undef, $new_table_name, $id_name);
165              
166             # A generic JOIN solution
167 0         0 return join("\n",
168             "INSERT INTO $new_table_name_quote",
169             "($column_list_str)",
170             "SELECT",
171             " $column_list_str",
172             "FROM",
173             " $orig_table_name_quote",
174             " LEFT JOIN $new_table_name_quote ON (".join(" = ", $old_full_id_name_quote, $new_full_id_name_quote).")",
175             "WHERE",
176             " $old_full_id_name_quote BETWEEN ? AND ? AND",
177             " $new_full_id_name_quote IS NULL",
178             );
179             }
180              
181             #pod =head2 post_connection_stmts
182             #pod
183             #pod @stmts = $helper->post_connection_stmts;
184             #pod
185             #pod These are the SQL statements to run right after a C<$dbh> re-connect, typically session
186             #pod variable set statements.
187             #pod
188             #pod =cut
189              
190             sub post_connection_stmts {
191             # No statements by default
192 0     0 1 0 return;
193             }
194              
195             #pod =head2 is_error_retryable
196             #pod
197             #pod $bool = $helper->is_error_retryable($error);
198             #pod
199             #pod Returns true if the specified error string (or exception object from DBIC/D:C:R) is
200             #pod retryable. Retryable errors generally fall under the categories of: lock contentions,
201             #pod lost DB connections, and query interruptions.
202             #pod
203             #pod =cut
204              
205             sub is_error_retryable {
206 0     0 1 0 warn sprintf "Not sure how to inspect DB errors for %s systems!", shift->dbms_name;
207 0         0 return 0;
208             }
209              
210             #pod =head2 create_table_sql
211             #pod
212             #pod $sql = $helper->create_table_sql($table_name);
213             #pod
214             #pod Get the C SQL statement for the specified table. This is RDBMS-specific,
215             #pod since C isn't always available and usually doesn't house all of the
216             #pod details, anyway.
217             #pod
218             #pod =cut
219              
220             sub create_table_sql {
221 0     0 1 0 die sprintf "Not sure how to create a new table for %s systems!", shift->dbms_name;
222             }
223              
224             #pod =head2 rename_fks_in_table_sql
225             #pod
226             #pod $sql = $helper->rename_fks_in_table_sql($table_name, $sql)
227             #pod if $helper->dbms_uses_global_fk_namespace;
228             #pod
229             #pod Given the C SQL, return the statement with the FKs renamed. This should
230             #pod use C to find a valid name.
231             #pod
232             #pod Only used if L is true.
233             #pod
234             #pod =cut
235              
236             sub rename_fks_in_table_sql {
237 0     0 1 0 my ($self, $table_name, $table_sql) = @_;
238              
239             # Don't change it by default
240 0         0 return $table_sql;
241             }
242              
243             #pod =head2 has_conflicting_triggers_on_table
244             #pod
245             #pod die if $helper->has_conflicting_triggers_on_table($table_name);
246             #pod
247             #pod Return true if triggers exist on the given table that would conflict with the operation.
248             #pod
249             #pod =cut
250              
251             sub has_conflicting_triggers_on_table {
252 0     0 1 0 die sprintf "Not sure how to check for table triggers for %s systems!", shift->dbms_name;
253             }
254              
255             #pod =head2 find_new_trigger_identifier
256             #pod
257             #pod $trigger_name = $helper->find_new_trigger_identifier($trigger_name);
258             #pod
259             #pod Return a free trigger identifier to use in the new trigger, using the inputted name as a
260             #pod base. This should use C to find a valid name.
261             #pod
262             #pod =cut
263              
264             sub find_new_trigger_identifier {
265 0     0 1 0 die sprintf "Not sure how to check for table triggers for %s systems!", shift->dbms_name;
266             }
267              
268             #pod =head2 modify_trigger_dml_stmts
269             #pod
270             #pod $helper->modify_trigger_dml_stmts( \%trigger_dml_stmts );
271             #pod
272             #pod Given the DML SQL statements to be plugged into the triggers, mutate the statements,
273             #pod tailored to the RDBMS. The input is a hashref of SQL statements for the following keys:
274             #pod
275             #pod replace # used in the INSERT/UPDATE triggers
276             #pod delete_for_update # used in the UPDATE trigger
277             #pod delete_for_delete # used in the DELETE trigger
278             #pod
279             #pod Since it's already a reference, this method will mutate the SQL strings.
280             #pod
281             #pod =cut
282              
283             sub modify_trigger_dml_stmts {
284 27     27 1 65 my $self = shift;
285              
286             # Don't change them by default
287 27         71 return @_;
288             }
289              
290             #pod =head2 analyze_table
291             #pod
292             #pod $helper->analyze_table($table_name);
293             #pod
294             #pod Run the DDL statement to re-analyze the table, typically C.
295             #pod
296             #pod =cut
297              
298             sub analyze_table {
299 0     0 1 0 my ($self, $table_name) = @_;
300 0         0 my $table_name_quote = $self->dbh->quote_identifier($table_name);
301 0         0 $self->dbh_runner_do("ANALYZE TABLE $table_name_quote");
302             }
303              
304             #pod =head2 swap_tables
305             #pod
306             #pod $helper->swap_tables($new_table_name, $orig_table_name, $old_table_name);
307             #pod
308             #pod Runs the SQL to swap the tables in a safe and atomic manner. The default ANSI SQL
309             #pod solution is to run two C statements in a transaction, but only if the RDBMS
310             #pod supports transactional DDL.
311             #pod
312             #pod =cut
313              
314             sub swap_tables {
315 27     27 1 128 my ($self, $new_table_name, $orig_table_name, $old_table_name) = @_;
316 27         486 my $dbh = $self->dbh;
317              
318             # If the RDBMS actually has a value for SQL_TXN_CAPABLE, and it's not SQL_TC_ALL,
319             # then it really doesn't support transactional DDL.
320 27         745150 my $txn_capable = $dbh->get_info( $GetInfoType{SQL_TXN_CAPABLE} );
321 27         488 my $sql_tc_all = $DBI::Const::GetInfo::ODBC::ReturnValues{SQL_TXN_CAPABLE}{SQL_TC_ALL};
322 27 50 33     225 if (defined $txn_capable && $txn_capable != $sql_tc_all) {
323 0         0 die sprintf "Not sure how to swap tables for %s systems!", shift->dbms_name;
324             }
325              
326 27         134 my $new_table_name_quote = $dbh->quote_identifier($new_table_name);
327 27         1049 my $orig_table_name_quote = $dbh->quote_identifier($orig_table_name);
328 27         609 my $old_table_name_quote = $dbh->quote_identifier($old_table_name);
329              
330             $self->dbh_runner(txn => set_subname '_table_swap', sub {
331 27     27   93 $dbh = $_;
332 27         179 $dbh->do("ALTER TABLE $orig_table_name_quote RENAME TO $old_table_name_quote");
333 27         119161 $dbh->do("ALTER TABLE $new_table_name_quote RENAME TO $orig_table_name_quote");
334 27         1351 });
335             }
336              
337             #pod =head2 foreign_key_info
338             #pod
339             #pod my $sth = $helper->foreign_key_info(
340             #pod $pk_catalog, $pk_schema, $pk_table_name,
341             #pod $fk_catalog, $fk_schema, $fk_table_name
342             #pod );
343             #pod
344             #pod Returns a statement handle in the same manner as a L call. In the
345             #pod default case, this is just that call, but certain implementations may need it to be
346             #pod overloaded or overridden.
347             #pod
348             #pod =cut
349              
350             sub foreign_key_info {
351 0     0 1   my $self = shift;
352 0           return $self->dbh->foreign_key_info(@_);
353             }
354              
355             #pod =head2 remove_fks_from_child_tables_stmts
356             #pod
357             #pod @stmts = $helper->remove_fks_from_child_tables_stmts if $helper->child_fks_need_adjusting;
358             #pod
359             #pod Return a list of statements needed to remove FKs from the child tables. These will be
360             #pod run through L.
361             #pod
362             #pod Only used if L is true.
363             #pod
364             #pod =cut
365              
366             sub remove_fks_from_child_tables_stmts {
367 0     0 1   my $self = shift;
368 0           my $dbh = $self->dbh;
369 0           my $fk_hash = $self->vars->{foreign_keys}{definitions};
370              
371 0           my @stmts;
372 0           foreach my $tbl_fk_name (sort keys %{$fk_hash->{child}}) {
  0            
373 0           my $fk = $fk_hash->{child}{$tbl_fk_name};
374              
375             # Ignore self-joined FKs
376 0 0 0       next if $fk->{fk_table_name} eq $self->table_name || $fk->{fk_table_name} eq $self->new_table_name;
377              
378             # ANSI SQL, of course
379             push @stmts, join(' ',
380             'ALTER TABLE',
381             $dbh->quote_identifier( $fk->{fk_table_name} ),
382             'DROP CONSTRAINT',
383 0           $dbh->quote_identifier( $fk->{fk_name} ),
384             );
385             }
386              
387 0           return @stmts;
388             }
389              
390             #pod =head2 rename_fks_back_to_original_stmts
391             #pod
392             #pod @stmts = $helper->rename_fks_back_to_original_stmts if $helper->dbms_uses_global_fk_namespace;
393             #pod
394             #pod Return a list of statements needed to rename the FKs back to their original names. These will be
395             #pod run through L.
396             #pod
397             #pod Only used if L is true.
398             #pod
399             #pod =cut
400              
401             sub rename_fks_back_to_original_stmts {
402 0     0 1   my $self = shift;
403 0           my $dbh = $self->dbh;
404 0           my $fks = $self->vars->{foreign_keys};
405 0           my $fk_hash = $fks->{definitions};
406              
407 0           my $table_name = $self->table_name;
408              
409 0           my @stmts;
410 0           foreach my $tbl_fk_name (sort keys %{$fk_hash->{parent}}) {
  0            
411 0           my $fk = $fk_hash->{parent}{$tbl_fk_name};
412              
413 0           my $changed_fk_name = $fk->{fk_name};
414 0           my $orig_fk_name = $fks->{orig_names}{"$table_name.$changed_fk_name"};
415              
416 0 0         unless ($orig_fk_name) {
417 0           $self->progress->message("WARNING: Did not find original FK name for $table_name.$changed_fk_name!");
418 0           next;
419             }
420              
421             # _fk_to_sql uses this directly, so just change it at the $fk hashref
422 0           $fk->{fk_name} = $orig_fk_name;
423              
424 0           push @stmts, join("\n",
425             "ALTER TABLE ".$dbh->quote_identifier($table_name),
426             " DROP CONSTRAINT ".$dbh->quote_identifier( $changed_fk_name ).',',
427             " ADD CONSTRAINT ".$self->fk_to_sql($fk)
428             );
429             }
430              
431 0           return @stmts;
432             }
433              
434             #pod =head2 add_fks_back_to_child_tables_stmts
435             #pod
436             #pod @stmts = $helper->add_fks_back_to_child_tables_stmts if $helper->child_fks_need_adjusting;
437             #pod
438             #pod Return a list of statements needed to add FKs back to the child tables. These will be
439             #pod run through L.
440             #pod
441             #pod Only used if L is true.
442             #pod
443             #pod =cut
444              
445             sub add_fks_back_to_child_tables_stmts {
446 0     0 1   my $self = shift;
447 0           my $dbh = $self->dbh;
448 0           my $fk_hash = $self->vars->{foreign_keys}{definitions};
449              
450 0           my @stmts;
451 0           foreach my $tbl_fk_name (sort keys %{$fk_hash->{child}}) {
  0            
452 0           my $fk = $fk_hash->{child}{$tbl_fk_name};
453              
454             # Ignore self-joined FKs
455 0 0 0       next if $fk->{fk_table_name} eq $self->table_name || $fk->{fk_table_name} eq $self->new_table_name;
456              
457             $self->dbh_runner_do(join ' ',
458             "ALTER TABLE",
459 0           $dbh->quote_identifier( $fk->{fk_table_name} ),
460             "ADD CONSTRAINT",
461             $self->fk_to_sql($fk),
462             );
463             }
464              
465 0           return @stmts;
466             }
467              
468             #pod =head2 post_fk_add_cleanup_stmts
469             #pod
470             #pod @stmts = $helper->post_fk_add_cleanup_stmts if $helper->child_fks_need_adjusting;
471             #pod
472             #pod Return a list of clean up statements to run after the FKs are re-added back to the child
473             #pod tables. These will be run through L.
474             #pod
475             #pod Only used if L is true. The base method does nothing.
476             #pod
477             #pod =cut
478              
479 0     0 1   sub post_fk_add_cleanup_stmts { return () }
480              
481             1;
482              
483             __END__