File Coverage

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