#!/usr/bin/perl use lib qw(t/lib); use strict; use warnings; use Test::OnlineDDL; ############################################################ my $CHUNK_SIZE = $CDTEST_MASS_POPULATE ? 5000 : 3; my $dbms_name = CDTest->dbms_name; ############################################################ onlineddl_test 'No-op copy' => 'Track' => sub { my $cd_schema = shift; my $track_rsrc = $cd_schema->source('Track'); my $dbh = $cd_schema->storage->dbh; # Constructor my $online_ddl = DBIx::OnlineDDL->new( rsrc => $track_rsrc, # purposely not adding any (useful) coderef_hooks coderef_hooks => { before_triggers => sub {} }, copy_opts => { chunk_size => $CHUNK_SIZE, }, ); is $online_ddl->table_name, 'track', 'Figured out table_name'; is $online_ddl->new_table_name, '_track_new', 'Figured out new_table_name'; my $helper = $online_ddl->_helper; my $mmver = $helper->mmver; my $orig_table_track_sql = $helper->create_table_sql('track'); my $orig_table_lyrics_sql = $helper->create_table_sql('lyrics'); # has FK pointing to track try_ok { $online_ddl->execute } 'Execute works'; is $online_ddl->copy_opts->{id_name}, 'trackid', 'Figured out PK'; my $new_table_track_sql = $helper->create_table_sql('track'); my $new_table_lyrics_sql = $helper->create_table_sql('lyrics'); # Remove AUTO_INCREMENT information $orig_table_track_sql =~ s/ AUTO_INCREMENT=\K\d+/###/; $orig_table_lyrics_sql =~ s/ AUTO_INCREMENT=\K\d+/###/; $new_table_track_sql =~ s/ AUTO_INCREMENT=\K\d+/###/; $new_table_lyrics_sql =~ s/ AUTO_INCREMENT=\K\d+/###/; is $new_table_track_sql, $orig_table_track_sql, 'New table SQL for `track` matches the old one'; SKIP: { skip "MySQL versions below 5.7 cannot fix the index problem", 1 if $dbms_name eq 'MySQL' && $mmver < 5.007; is $new_table_lyrics_sql, $orig_table_lyrics_sql, 'New table SQL for `lyrics` matches the old one'; }; }; onlineddl_test 'Existing triggers' => 'Track' => sub { my $cd_schema = shift; my $track_rsrc = $cd_schema->source('Track'); my $dbh = $cd_schema->storage->dbh; # Constructor (another no-op) my $online_ddl = DBIx::OnlineDDL->new( rsrc => $track_rsrc, coderef_hooks => { before_triggers => sub {} }, copy_opts => { chunk_size => $CHUNK_SIZE, }, ); my $helper = $online_ddl->_helper; my $mmver = $helper->mmver; # Add a few new triggers my (@trigger_sql, @trigger_qnames); foreach my $trigger_type (qw< INSERT UPDATE DELETE >) { my $trigger_name = $helper->find_new_trigger_identifier( "track_oddltest_".lc($trigger_type) ); my $trigger_qname = $dbh->quote_identifier($trigger_name); my $table_qname = $dbh->quote_identifier('track'); push @trigger_sql, join("\n", "CREATE TRIGGER $trigger_qname BEFORE $trigger_type ON $table_qname FOR EACH ROW", 'BEGIN', # SQLite doesn't like empty procedures in its triggers. MySQL is fine with them, but doesn't like # returning a result set from a trigger. ($dbms_name eq 'SQLite' ? 'SELECT 1;' : ''), 'END' ); push @trigger_qnames, $trigger_qname; } try_ok { $online_ddl->dbh_runner_do(@trigger_sql); } 'Triggers created'; my $should_execute = $dbms_name eq 'MySQL' && $mmver >= 5.007; if ($should_execute) { try_ok { $online_ddl->execute } 'Execute works'; } else { like( dies { $online_ddl->execute }, qr, 'Execute dies due to triggers', ); } # Get rid of the triggers try_ok { $online_ddl->dbh_runner_do( map { "DROP TRIGGER IF EXISTS $_" } @trigger_qnames ); } 'Triggers dropped'; }; onlineddl_test 'Add column' => 'Track' => sub { my $cd_schema = shift; my $track_rsrc = $cd_schema->source('Track'); # Constructor my $online_ddl = DBIx::OnlineDDL->new( rsrc => $track_rsrc, coderef_hooks => { before_triggers => sub { my $oddl = shift; my $dbh = $oddl->dbh; my $name = $oddl->new_table_name; my $qname = $dbh->quote_identifier($name); my $qcol = $dbh->quote_identifier('test_column'); $oddl->dbh_runner_do("ALTER TABLE $qname ADD COLUMN $qcol VARCHAR(100) NULL"); }, }, copy_opts => { chunk_size => $CHUNK_SIZE, }, ); try_ok { $online_ddl->execute } 'Execute works'; # Verify the column exists my $dbh = $cd_schema->storage->dbh; my $vars = $online_ddl->_vars; my $catalog = $vars->{catalog}; my $schema = $vars->{schema}; my %cols = %{ $dbh->column_info( $catalog, $schema, 'track', '%' )->fetchall_hashref('COLUMN_NAME') }; like( $cols{test_column}, { TABLE_CAT => $catalog, TABLE_SCHEM => $schema, TABLE_NAME => 'track', COLUMN_NAME => 'test_column', COLUMN_SIZE => 100, TYPE_NAME => 'VARCHAR', IS_NULLABLE => 'YES', NULLABLE => 1, ORDINAL_POSITION => 7, }, 'New column exists in table', ); }; onlineddl_test 'Add column + title change' => 'Track' => sub { my $cd_schema = shift; my $track_rsrc = $cd_schema->source('Track'); # Constructor my $online_ddl = DBIx::OnlineDDL->new( rsrc => $track_rsrc, coderef_hooks => { before_triggers => sub { my $oddl = shift; my $dbh = $oddl->dbh; my $name = $oddl->new_table_name; my $qname = $dbh->quote_identifier($name); my $qcol = $dbh->quote_identifier('test_column'); my $qidx = $dbh->quote_identifier('track_cd_title'); $oddl->dbh_runner_do( "ALTER TABLE $qname ADD COLUMN $qcol VARCHAR(100) NULL", # SQLite can't DROP on an ALTER TABLE, but isn't bothered by the breaking of # a unique index (for some reason) ($dbms_name eq 'SQLite' ? () : "ALTER TABLE $qname DROP INDEX $qidx" ) ); }, before_swap => sub { my $oddl = shift; my $dbh = $oddl->dbh; my $name = $oddl->new_table_name; my $qname = $dbh->quote_identifier($name); DBIx::BatchChunker->construct_and_execute( chunk_size => $CHUNK_SIZE, process_past_max => 1, dbic_storage => $oddl->rsrc->storage, min_stmt => "SELECT MIN(trackid) FROM $qname", max_stmt => "SELECT MAX(trackid) FROM $qname", stmt => join( ' ', 'UPDATE', $dbh->quote_identifier($name), 'SET title =', $dbh->quote('This is the song that never ends'), 'WHERE trackid BETWEEN ? AND ?', ), ); }, }, copy_opts => { chunk_size => $CHUNK_SIZE, }, ); try_ok { $online_ddl->execute } 'Execute works'; # Verify the column exists my $dbh = $cd_schema->storage->dbh; my $vars = $online_ddl->_vars; my $catalog = $vars->{catalog}; my $schema = $vars->{schema}; my %cols = %{ $dbh->column_info( $catalog, $schema, 'track', '%' )->fetchall_hashref('COLUMN_NAME') }; like( $cols{test_column}, { TABLE_CAT => $catalog, TABLE_SCHEM => $schema, TABLE_NAME => 'track', COLUMN_NAME => 'test_column', COLUMN_SIZE => 100, TYPE_NAME => 'VARCHAR', IS_NULLABLE => 'YES', NULLABLE => 1, ORDINAL_POSITION => 7, }, 'New column exists in table', ); }; onlineddl_test 'Drop column' => 'Track' => sub { plan skip_all => 'SQLite cannot drop columns' if $dbms_name eq 'SQLite'; my $cd_schema = shift; my $track_rsrc = $cd_schema->source('Track'); # Constructor my $online_ddl = DBIx::OnlineDDL->new( rsrc => $track_rsrc, coderef_hooks => { before_triggers => sub { my $oddl = shift; my $dbh = $oddl->dbh; my $name = $oddl->new_table_name; my $qname = $dbh->quote_identifier($name); my $qcol = $dbh->quote_identifier('last_updated_at'); $oddl->dbh_runner_do("ALTER TABLE $qname DROP COLUMN $qcol"); }, }, copy_opts => { chunk_size => $CHUNK_SIZE, }, ); try_ok { $online_ddl->execute } 'Execute works'; # Verify the column doesn't exist my $dbh = $cd_schema->storage->dbh; my $vars = $online_ddl->_vars; my $catalog = $vars->{catalog}; my $schema = $vars->{schema}; my %cols = %{ $dbh->column_info( $catalog, $schema, 'track', '%' )->fetchall_hashref('COLUMN_NAME') }; ok(!exists $cols{last_updated_at}, 'Column dropped in table', $cols{last_updated_at}); }; onlineddl_test 'Drop PK' => 'Track' => sub { plan skip_all => 'SQLite cannot drop columns' if $dbms_name eq 'SQLite'; my $cd_schema = shift; my $track_rsrc = $cd_schema->source('Track'); # Constructor my $online_ddl = DBIx::OnlineDDL->new( rsrc => $track_rsrc, coderef_hooks => { before_triggers => sub { my $oddl = shift; my $dbh = $oddl->dbh; my $name = $oddl->new_table_name; my $qname = $dbh->quote_identifier($name); my $qcol = $dbh->quote_identifier('trackid'); $oddl->dbh_runner_do("ALTER TABLE $qname DROP COLUMN $qcol"); my $fk_hash = $oddl->dbh_runner(run => sub { # Need to also drop the FK on lyrics return $oddl->_fk_info_to_hash( $oddl->_helper->foreign_key_info( $oddl->_vars->{catalog}, $oddl->_vars->{schema}, $oddl->table_name, undef, undef, undef ) ); }); $oddl->dbh_runner_do(join ' ', 'ALTER TABLE', $dbh->quote_identifier('lyrics'), 'DROP', # MySQL uses 'FOREIGN KEY' on DROPs, and everybody else uses 'CONSTRAINT' on both ($dbms_name eq 'MySQL' ? 'FOREIGN KEY' : 'CONSTRAINT'), $dbh->quote_identifier( (values %$fk_hash)[0]->{fk_name} ), ); }, }, copy_opts => { chunk_size => $CHUNK_SIZE, }, ); try_ok { $online_ddl->execute } 'Execute works'; # Verify the column doesn't exist my $dbh = $cd_schema->storage->dbh; my $vars = $online_ddl->_vars; my $catalog = $vars->{catalog}; my $schema = $vars->{schema}; my %cols = %{ $dbh->column_info( $catalog, $schema, 'track', '%' )->fetchall_hashref('COLUMN_NAME') }; ok(!exists $cols{trackid}, 'PK column dropped in table', $cols{trackid}); }; ############################################################ done_testing;