#!/usr/bin/env perl #=============================================================================== # # FILE: copy_mysql_pg # # USAGE: ./copy_mysql_pg # # DESCRIPTION: Copies HPR show data from the MariaDB database to an # experimental PostgreSQL database # # OPTIONS: --- # REQUIREMENTS: --- # BUGS: --- # NOTES: --- # AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com # VERSION: 0.0.4 # CREATED: 2017-03-15 18:50:08 # REVISION: 2017-10-17 22:37:08 # #=============================================================================== use 5.010; use strict; use warnings; use utf8; use Getopt::Long; use Pod::Usage; use Config::General; use List::MoreUtils qw{uniq apply}; use Text::CSV; use DBI; use Data::Dumper; # # Version number (manually incremented) # our $VERSION = '0.0.4'; # # Script and directory names # ( my $PROG = $0 ) =~ s|.*/||mx; ( my $DIR = $0 ) =~ s|/?[^/]*$||mx; $DIR = '.' unless $DIR; #------------------------------------------------------------------------------- # Declarations #------------------------------------------------------------------------------- # # Constants and other declarations # my $basedir = "$ENV{HOME}/HPR/Database"; my $configfile1 = "$basedir/.hpr_db.cfg"; my $configfile2 = "$basedir/.hpr_pg.cfg"; my $email_template = 'host_%s@hackerpublicradio.org'; my ( $dbh1, $sth1, $h1, $rv1 ); my ( $dbh2, $sth2, $h2, $rv2 ); my (@phase_choices); my ( %eps_tags, %data ); my @phases = ( 'episodes', 'hosts', 'eh_xref', 'series', 'es_xref', 'tags', 'comments', 'twat', 'epilogue' ); # # Enable Unicode mode # binmode STDOUT, ":encoding(UTF-8)"; binmode STDERR, ":encoding(UTF-8)"; # # Load database configuration data # my $conf1 = Config::General->new( -ConfigFile => $configfile1, -InterPolateVars => 1, -ExtendedAccess => 1 ); my %config1 = $conf1->getall(); my $conf2 = Config::General->new( -ConfigFile => $configfile2, -InterPolateVars => 1, -ExtendedAccess => 1 ); my %config2 = $conf2->getall(); #------------------------------------------------------------------------------- # Options and arguments #------------------------------------------------------------------------------- my $DEF_DEBUG = 0; # # Process options # my %options; Options( \%options ); # # Default help # pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 ) if ( $options{'help'} ); # # Collect options # my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG ); my $verbose = ( defined( $options{verbose} ) ? $options{verbose} : 0 ); my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 0 ); # # This option is a list, provided as a CSV # my $phase_choices = $options{phases}; if ( defined($phase_choices) ) { # # We have a list which we'll parse, validate, sort, make unique and filter # my $lcsv = Text::CSV_XS->new( { binary => 1, } ); if ( $lcsv->parse($phase_choices) ) { # Sort fields @phase_choices = uniq( sort { $a cmp $b } $lcsv->fields() ); # Trim leading and trailing spaces @phase_choices = apply { $_ =~ s/(^\s*|\s*$)// } @phase_choices; # Make a list of invalid keywords my %tmp = map { $_ => 1 } @phases; my @bad = grep { not exists $tmp{$_} } @phase_choices; # Deal with all errors die "Invalid list; no elements\n" if scalar(@phase_choices) == 0; die "Invalid list; too many elements\n" if scalar(@phase_choices) > scalar(@phases); die "Invalid list elements: ", join( ",", @bad ) . "\n" if scalar(@bad) > 0; } else { die "Failed to parse -list='$phase_choices'\n" . $lcsv->error_diag() . "\n"; } } else { # # By default we do all phases # @phase_choices = @phases; } #------------------------------------------------------------------------------- # Connect to the MariaDB database #------------------------------------------------------------------------------- my $dbtype1 = $config1{database}->{type} // 'mysql'; my $dbhost1 = $config1{database}->{host} // '127.0.0.1'; my $dbport1 = $config1{database}->{port} // 3306; my $dbname1 = $config1{database}->{name}; my $dbuser1 = $config1{database}->{user}; my $dbpwd1 = $config1{database}->{password}; $dbh1 = DBI->connect( "dbi:$dbtype1:host=$dbhost1;port=$dbport1;database=$dbname1", $dbuser1, $dbpwd1, { AutoCommit => 1 } ) or die $DBI::errstr; # # Enable client-side UTF8 # $dbh1->{mysql_enable_utf8} = 1; #------------------------------------------------------------------------------- # Connect to the PostgreSQL database #------------------------------------------------------------------------------- my $dbtype2 = $config2{database}->{type} // 'Pg'; my $dbhost2 = $config2{database}->{host} // '127.0.0.1'; my $dbport2 = $config2{database}->{port} // 5432; my $dbname2 = $config2{database}->{name}; my $dbuser2 = $config2{database}->{user}; my $dbpwd2 = $config2{database}->{password}; $dbh2 = DBI->connect( "dbi:$dbtype2:host=$dbhost2;database=$dbname2;port=$dbport2", $dbuser2, $dbpwd2, { PrintError => 0, AutoCommit => 1 } ) or die $DBI::errstr; # # Enable client-side UTF8 # $dbh2->{pg_enable_utf8} = 1; my %choices = map { $_ => 1 } @phase_choices; # # Perform phases in order, omitting those that are not in the list # for my $phase (@phases) { #--------------------------------------------------------------------------- # Copy the 'eps' table to 'episodes' #--------------------------------------------------------------------------- if ( $phase eq 'episodes' && exists( $choices{$phase} ) ) { print "Build episodes table\n" if ( $verbose > 0 ); if ( check_table( $dbh2, 'episodes' ) ) { build_episodes_table( $dbh1, $dbh2, $verbose ) unless $dry_run; } else { print "** Table 'episodes' is not empty\n"; } } #--------------------------------------------------------------------------- # Copy the 'hosts' table to 'hosts' #--------------------------------------------------------------------------- elsif ( $phase eq 'hosts' && exists( $choices{$phase} ) ) { print "Build hosts table\n" if ( $verbose > 0 ); if ( check_table( $dbh2, 'hosts' ) ) { build_hosts_table( $dbh1, $dbh2, $verbose ) unless $dry_run; } else { print "** Table 'hosts' is not empty\n"; } } #--------------------------------------------------------------------------- # Generate the 'episodes_hosts_xref' table #--------------------------------------------------------------------------- elsif ( $phase eq 'eh_xref' && exists( $choices{$phase} ) ) { print "Build episodes_hosts_xref table\n" if ( $verbose > 0 ); if ( check_table( $dbh2, 'episodes_hosts_xref' ) ) { build_episodes_hosts_xref_table( $dbh1, $dbh2, $verbose ) unless $dry_run; } else { print "** Table 'episodes_hosts_xref' is not empty\n"; } } #--------------------------------------------------------------------------- # Copy the 'miniseries' table to 'series' #--------------------------------------------------------------------------- elsif ( $phase eq 'series' && exists( $choices{$phase} ) ) { print "Build series table\n" if ( $verbose > 0 ); if ( check_table( $dbh2, 'series' ) ) { build_series_table( $dbh1, $dbh2, $verbose ) unless $dry_run; } else { print "** Table 'series' is not empty\n"; } } #--------------------------------------------------------------------------- # Generate the 'episodes_series_xref' table #--------------------------------------------------------------------------- elsif ( $phase eq 'es_xref' && exists( $choices{$phase} ) ) { print "Build episodes_series_xref table\n" if ( $verbose > 0 ); if ( check_table( $dbh2, 'episodes_series_xref' ) ) { build_episodes_series_xref_table( $dbh1, $dbh2, $verbose ) unless $dry_run; } else { print "** Table 'episodes_series_xref' is not empty\n"; } } #--------------------------------------------------------------------------- # Collect and store the id numbers and tags from the 'eps' table, then add # them to the PostgreSQL tables. #--------------------------------------------------------------------------- elsif ( $phase eq 'tags' && exists( $choices{$phase} ) ) { print "Build tags and episodes_tags_xref tables\n" if ( $verbose > 0 ); if ( check_table( $dbh2, 'tags' ) && check_table( $dbh2, 'episodes_tags_xref' ) ) { unless ($dry_run) { %eps_tags = %{ collect_eps_tags( $dbh1, $verbose ) }; if (%eps_tags) { build_tags_table( $dbh2, $verbose, \%eps_tags ); } } } else { print "** Tables 'tags' and/or 'episodes_tags_xref' are not empty\n"; } } #--------------------------------------------------------------------------- # Copy the 'comments' table to 'comments' #--------------------------------------------------------------------------- elsif ( $phase eq 'comments' && exists( $choices{$phase} ) ) { print "Build comments table\n" if ( $verbose > 0 ); if (check_table($dbh2,'comments')) { build_comments_table( $dbh1, $dbh2, $verbose ) unless $dry_run; } else { print "** Table 'comments' is not empty\n"; } } #--------------------------------------------------------------------------- # Perform the 'twat' actions #--------------------------------------------------------------------------- elsif ( $phase eq 'twat' && exists( $choices{$phase} ) ) { print "Perform twat actions\n" if ( $verbose > 0 ); # # Incorporate the TwaT tables (assuming they exist) # if (check_table($dbh2,'twat_hosts')) { load_twat_hosts($dbh1,$dbh2,$verbose) unless $dry_run; } else { print "** Table 'twat_hosts' is not empty\n"; } if (check_table($dbh2,'twat_episodes')) { load_twat_episodes($dbh1,$dbh2,$verbose) unless $dry_run; } else { print "** Table 'twat_episodes' is not empty\n"; } } #--------------------------------------------------------------------------- # Perform the 'epilogue' actions #--------------------------------------------------------------------------- elsif ( $phase eq 'epilogue' && exists( $choices{$phase} ) ) { print "Perform epilogue actions\n" if ( $verbose > 0 ); # # Determine the first show date per host, assuming that's when they # were added to the database # compute_host_date_added($dbh2) unless $dry_run; } } exit; #=== FUNCTION ================================================================ # NAME: build_episodes_table # PURPOSE: Copy the data from the MariaDB 'eps' table to the Pg # 'episodes' table # PARAMETERS: $dbh1 Handle for the MariaDB database # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_episodes_table { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); $sth1 = $dbh1->prepare('SELECT * FROM eps') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } $sth2 = $dbh2->prepare('INSERT INTO episodes VALUES (?,?,?,?,?,?,?,?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } # # Query MariaDB for the entire 'eps' table # $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } # # Loop though 'eps' table rows writing them to the PostgreSQL 'episodes' # table # $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{id}, $h1->{date}, $h1->{title}, nullif( $h1->{summary}, '^\s*$' ), $h1->{notes}, $h1->{explicit}, $h1->{license}, $h1->{duration}, $h1->{downloads}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ($verbose > 0); # # Set the sequence to the correct value # alter_seq($dbh2,'episodes','episode_seq'); } #=== FUNCTION ================================================================ # NAME: build_hosts_table # PURPOSE: Copy the data from the Mariadb 'hosts' table to the Pg 'hosts' # table # PARAMETERS: $dbh1 Handle for the MariaDB table # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_hosts_table { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $rv, $count ); $sth1 = $dbh1->prepare('SELECT * FROM hosts') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } $sth2 = $dbh2->prepare( q{INSERT INTO hosts (id, host, email, profile, license, local_image, gpg, valid) VALUES (?,?,?,?,?,?,?,?)} ) or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{hostid}, $h1->{host}, default_email( $h1->{email}, '^(\s*|admin@hackerpublicradio.org)$', $email_template, $h1->{hostid} ), nullif( $h1->{profile}, '^\s*$' ), $h1->{license}, $h1->{local_image}, nullif( $h1->{gpg}, '^\s*$' ), $h1->{valid}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ( $verbose > 0 ); # # Set the sequence to the correct value # alter_seq( $dbh2, 'hosts', 'host_seq' ); } #=== FUNCTION ================================================================ # NAME: build_episodes_hosts_xref_table # PURPOSE: Generates the cross reference table by examining the 'eps' and # 'hosts' tables in the MariaDB database. # PARAMETERS: $dbh1 Handle for the MariaDB table # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_episodes_hosts_xref_table { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); $sth1 = $dbh1->prepare( 'SELECT e.id,h.hostid FROM eps e, hosts h WHERE e.hostid = h.hostid') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } $sth2 = $dbh2->prepare('INSERT INTO episodes_hosts_xref VALUES (?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{id}, $h1->{hostid}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ($verbose > 0); } #=== FUNCTION ================================================================ # NAME: build_series_table # PURPOSE: Copy the data from the Mariadb 'miniseries' table to the Pg # 'series' table # PARAMETERS: $dbh1 Handle for the MariaDB database # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_series_table { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); $sth1 = $dbh1->prepare('SELECT * FROM miniseries') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } $sth2 = $dbh2->prepare('INSERT INTO series VALUES (?,?,?,?,?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } # # Query MariaDB for the entire 'miniseries' table # $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } # # Loop though 'miniseries' table rows writing them to the PostgreSQL # 'series' table # $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{id}, $h1->{name}, $h1->{description}, $h1->{private}, nullif( $h1->{image}, '^\s*$' ), $h1->{valid}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ($verbose > 0); # # Set the sequence to the correct value # alter_seq($dbh2,'series','series_seq'); } #=== FUNCTION ================================================================ # NAME: build_episodes_series_xref_table # PURPOSE: Generates the cross reference table by examining the 'eps' and # 'series' tables in the MariaDB database. # PARAMETERS: $dbh1 Handle for the MariaDB table # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_episodes_series_xref_table { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); $sth1 = $dbh1->prepare( 'SELECT e.id AS epid, m.id AS msid FROM eps e, miniseries m WHERE e.series = m.id') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } $sth2 = $dbh2->prepare('INSERT INTO episodes_series_xref VALUES (?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{epid}, $h1->{msid}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ($verbose > 0); } #=== FUNCTION ================================================================ # NAME: build_tags_table # PURPOSE: Using the data structure built from the MariaDB database # populate the many-to-many table in the Pg database # PARAMETERS: $dbh Handle for the Pg database # $verbose Verbosity level # $tag_hash Reference to a hash of episode ids and tags # for each episode # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_tags_table { my ( $dbh, $verbose, $tag_hash ) = @_; my ( $sth1, $h1, $sth2, $sth3, $rv, $tags, $tid, $count1, $count2 ); # # Query to find if a tag already exists # $sth1 = $dbh->prepare(q{SELECT * FROM tags WHERE tag = ?}); # # Query to add a new tag # $sth2 = $dbh->prepare(q{INSERT INTO tags (tag) VALUES(?)}); # # Query to add a new joining row # $sth3 = $dbh->prepare(q{INSERT INTO episodes_tags_xref VALUES(?,?)}); $count1 = $count2 = 0; foreach my $id ( sort { $a <=> $b } keys( %{$tag_hash} ) ) { # # Get the array of tags for this episode id # $tags = $tag_hash->{$id}; # # Loop through the array of tags (using an integer so we can index the # current tag) # for my $i ( 0 .. $#$tags ) { # # Look to see if this tag exists # $sth1->execute( $tags->[$i] ); if ( $dbh->err ) { warn $dbh->errstr; } # # If it's already in the table just store the id for later # otherwise add a new entry # if ( $h1 = $sth1->fetchrow_hashref ) { $tid = $h1->{id}; } else { # # Add the tag to 'tags' # $count1++; $rv = $sth2->execute( $tags->[$i] ); if ( $dbh->err ) { warn $dbh->errstr; } $rv = 0 if ( $rv eq '0E0' ); # # Get the id number of the inserted tag # $tid = $dbh->last_insert_id( undef, undef, undef, undef, { sequence => 'tag_seq' } ); } $count2++; $rv = $sth3->execute( $id, $tid ); if ( $dbh->err ) { warn $dbh->errstr; } $rv = 0 if ( $rv eq '0E0' ); } } if ($verbose > 0) { print "Added $count1 tags\n"; print "Added $count2 cross references\n"; } } #=== FUNCTION ================================================================ # NAME: collect_eps_tags # PURPOSE: Collects the tags from the eps.tags field # PARAMETERS: $dbh Database handle # $verbose Verbosity level # RETURNS: A reference to the hash created by collecting all the tags # DESCRIPTION: Queries the MariaDB 'eps' table for all of the rows containing # tags, returning the comma-separated list with the id number. # Each CSV list is then parsed and the result turned into a hash # keyed on the id number and containing a sorted array of tags. # If the level of verbosity is greater than 2 the tags has is # dumped (ironically, as a CSV list!). # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub collect_eps_tags { my ( $dbh, $verbose ) = @_; my ( $status, @fields, %hash ); my ( $sth, $h ); # # For parsing the field as CSV # my $csv = Text::CSV_XS->new; # # Query the MariaDB 'eps' table for all the id and tags # $sth = $dbh->prepare( q{SELECT id,tags FROM eps WHERE length(tags) > 0 ORDER BY id} ) or die $DBI::errstr; if ( $dbh->err ) { warn $dbh->errstr; } $sth->execute; if ( $dbh->err ) { warn $dbh->errstr; } # # Loop through what we got # while ( $h = $sth->fetchrow_hashref ) { # # Parse the tag list # $status = $csv->parse( $h->{tags} ); unless ($status) { # # Report any errors # print "Parse error on episode ", $h->{id}, "\n"; print $csv->error_input(), "\n"; next; } @fields = $csv->fields(); next unless (@fields); # # Trim all tags (don't alter $_ when doing it) # @fields = map { my $t = $_; $t =~ s/(^\s+|\s+$)//g; $t; } @fields; #print "$h->{id}: ",join(",",@fields),"\n"; # # Save the id and its tags, sorted for comparison # $hash{ $h->{id} } = [ sort @fields ]; } # # Dump all id numbers and tags if the verbose level is high enough # if ( $verbose >= 2 ) { print "\nTags collected from the 'eps' table\n\n"; foreach my $id ( sort { $a <=> $b } keys(%hash) ) { printf "%04d: %s\n", $id, join( ",", @{ $hash{$id} } ); } } return \%hash; } #=== FUNCTION ================================================================ # NAME: build_comments_table # PURPOSE: Copy the data from the Mariadb 'comments' table to the Pg # 'comments' table # PARAMETERS: $dbh1 Handle for the MariaDB database # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub build_comments_table { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); $sth1 = $dbh1->prepare('SELECT * FROM comments') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } $sth2 = $dbh2->prepare('INSERT INTO comments VALUES (?,?,?,?,?,?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } # # Query MariaDB for the entire 'comments' table # $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } # # Loop though 'comments' table rows writing them to the PostgreSQL # 'comments' table # $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{id}, $h1->{eps_id}, $h1->{comment_timestamp}, nullif( $h1->{comment_author_name}, '^\s*$' ), nullif( $h1->{comment_title}, '^\s*$' ), $h1->{comment_text}, $h1->{last_changed}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ($verbose > 0); # # Set the sequence to the correct value # alter_seq($dbh2,'comments','comment_seq'); } #=== FUNCTION ================================================================ # NAME: load_twat_hosts # PURPOSE: Copy the temporary twat_hosts table from the MySQL database # into the PostgreSQL equivalent ready for merging with the HPR # data # PARAMETERS: $dbh1 Handle for the MariaDB database # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub load_twat_hosts { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); # # Copy the 'twat_hosts' table # $sth1 = $dbh1->prepare('SELECT * FROM twat_hosts') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } # # Omit the 'id' here because it's an addition for PostgreSQL # $sth2 = $dbh2->prepare( 'INSERT INTO twat_hosts (host,email,website,repeat) VALUES (?,?,?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } # # Query MariaDB for the entire 'comments' table # $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } # # Loop though 'twat_hosts' table rows writing them to the PostgreSQL # 'twat_hosts' table # $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{host}, nullif( $h1->{email}, '^\s*$' ), nullif( $h1->{website}, '^\s*$' ), $h1->{repeat}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ( $verbose > 0 ); } #=== FUNCTION ================================================================ # NAME: load_twat_episodes # PURPOSE: Copy the temporary twat_episodes table from the MySQL database # into the PostgreSQL equivalent ready for merging with the HPR # data # PARAMETERS: $dbh1 Handle for the MariaDB database # $dbh2 Handle for the Pg database # $verbose Verbosity level # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub load_twat_episodes { my ( $dbh1, $dbh2, $verbose ) = @_; my ( $sth1, $h1, $sth2, $count ); # # Copy the 'twat_episodes' table # $sth1 = $dbh1->prepare('SELECT * FROM twat_episodes') or die $DBI::errstr; if ( $dbh1->err ) { warn $dbh1->errstr; } # # Omit the 'id' here because it's an addition for PostgreSQL # $sth2 = $dbh2->prepare( 'INSERT INTO twat_episodes VALUES (?,?,?,?,?,?)') or die $DBI::errstr; if ( $dbh2->err ) { warn $dbh2->errstr; } # # Query MariaDB for the entire 'comments' table # $sth1->execute; if ( $dbh1->err ) { die $dbh1->errstr; } # # Loop though 'twat_hosts' table rows writing them to the PostgreSQL # 'twat_hosts' table # $count = 0; while ( $h1 = $sth1->fetchrow_hashref ) { $count++; $sth2->execute( $h1->{ep_num}, $h1->{date}, $h1->{host}, $h1->{topic}, nullif( $h1->{writeup}, '^\s*$' ), $h1->{url}, ); if ( $dbh2->err ) { die $dbh2->errstr; } } print "Copied $count records\n" if ( $verbose > 0 ); } #=== FUNCTION ================================================================ # NAME: compute_host_date_added # PURPOSE: Determine the 'hosts.date_added' field once the database is # fully populated. # PARAMETERS: $dbh Handle for the PostgreSQL database # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub compute_host_date_added { my ($dbh) = @_; my $rv; # # Allocate date_added values where possible # $rv = $dbh->do( q{ UPDATE hosts SET date_added = sq.date_added FROM ( SELECT h.id,min(e.release_date) AS date_added FROM episodes e JOIN episodes_hosts_xref eh ON (e.id = eh.episodes_id) JOIN hosts h ON (h.id = eh.hosts_id) GROUP BY h.id ORDER by min(e.release_date)) AS sq WHERE hosts.id = sq.id } ); if ( $dbh->err ) { warn $dbh->errstr; } $rv = 0 if ( $rv eq '0E0' ); print "Added $rv dates to the 'date_added' column\n"; } #=== FUNCTION ================================================================ # NAME: alter_seq # PURPOSE: Ensure the PostgreSQL sequence associated with a table has the # correct value. # PARAMETERS: $dbh Handle for the PostgreSQL database # $table Table name for the query # $sequence Sequence name # RETURNS: Nothing # DESCRIPTION: # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub alter_seq { my ( $dbh, $table, $sequence ) = @_; my ( $sth, $h, $rv, $maxid ); # # Find the maximum id number in the table # $sth = $dbh->prepare("SELECT max(id) as maxid FROM $table") or die $DBI::errstr; if ( $dbh->err ) { warn $dbh->errstr; } $sth->execute; if ( $dbh->err ) { die $dbh->errstr; } # # Save the maximum # if ( $h = $sth->fetchrow_hashref ) { $maxid = $h->{maxid}; $sth->finish; } # # Reset the sequence one more than the maximum # $maxid++; $rv = $dbh->do("ALTER SEQUENCE $sequence RESTART WITH $maxid"); if ( $dbh->err ) { warn $dbh->errstr; } $rv = 0 if ( $rv eq '0E0' ); warn "Failed to reset $sequence\n" unless (defined($rv)); return; } #=== FUNCTION ================================================================ # NAME: check_table # PURPOSE: Check that a given PostgreSQL table is empty # PARAMETERS: $dbh Handle for the PostgreSQL database # $table Name of table # RETURNS: True if empty, otherwise false # DESCRIPTION: Simply perform a query on the nominated table which counts # rows. If the table does not exist a DBI method will fail (the # execute?), so we treat this as a 'no empty' to make the caller # take error action. # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub check_table { my ( $dbh, $table ) = @_; my ( $sth, $h, $count ); $sth = $dbh->prepare("SELECT count(*) AS count FROM $table") or die $DBI::errstr; if ( $dbh->err ) { warn $dbh->errstr; return 0; } $sth->execute; if ( $dbh->err ) { warn $dbh->errstr; return 0; } if ( $h = $sth->fetchrow_hashref ) { $count = $h->{count}; $sth->finish; } return $count == 0; } #=== FUNCTION ================================================================ # NAME: default_email # PURPOSE: Make a default email address for hosts with none # PARAMETERS: $email Original email address # $regex Regular expression to check the email against # $template Template for building the default # $hostid Host id number to use in the default # RETURNS: The email address to be used # DESCRIPTION: If the email address matches a regular expression then # generate a default from the template and the host id, # otherwise just return the address untouched. # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub default_email { my ( $email, $regex, $template, $hostid ) = @_; return ( $email =~ $regex ? sprintf( $template, $hostid ) : $email ); } #=== FUNCTION ================================================================ # NAME: nullif # PURPOSE: Tests a value and makes it 'undef' (equivalent to NULL in the # database) if it matches a regular expression. # PARAMETERS: $value Value to test # $regex Regular expression to match against # RETURNS: 'undef' if the values match, otherwise the original value # DESCRIPTION: This is very simple, just a wrapper around the test. # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub nullif { my ( $value, $regex ) = @_; return $value unless defined($value); return ( $value =~ $regex ? undef : $value ); } #=== FUNCTION ================================================================ # NAME: Options # PURPOSE: Processes command-line options # PARAMETERS: $optref Hash reference to hold the options # RETURNS: Undef # DESCRIPTION: # THROWS: no exceptions # COMMENTS: none # SEE ALSO: n/a #=============================================================================== sub Options { my ($optref) = @_; my @options = ( "help", "debug=i", "dry-run!", "verbose+", "phases=s" ); if ( !GetOptions( $optref, @options ) ) { pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 ); } return; } __END__ #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # Application Documentation #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% #{{{ =head1 NAME copy_mysql_pg - copy the HPR database from MySQL to PostgreSQL =head1 VERSION This documentation refers to B version 0.0.4 =head1 USAGE copy_mysql_pg -verbose copy_mysql_pg -verbose -verbose copy_mysql_pg -verbose \ -phase='episodes,hosts,eh_xref,series,es_xref,tags,comments,epilogue' =head1 OPTIONS =over 8 =item B<-help> Prints a brief help message describing the usage of the program, and then exits. =item B<-debug=N> Selects a level of debugging. Debug information consists of a line or series of lines prefixed with the characters 'D>': =over 4 =item B<0> No debug output is generated: this is the default =back =item B<-[no]dry-run> When enabled (B<-dry-run>) the script will report what it would do, but will make no changes to the target database. In the default state (B<-nodry-run>) then changes are made. =item B<-verbose> Makes the script verbose resulting in the production of more information about what it is doing. The option may be repeated to increase the level of verbosity. The levels are: =over 4 =item B<0> No output is generated (apart from errors and warnings if appropriate). This is the default level. =item B<1> A message is generated per phase to indicate which actions are taking place. This includes a report of the number of rows copied from the MySQL database to the PostgreSQL one. =item B<2> Following the process of collecting the CSV tags from the MySQL 'episodes' table these are reported as a list per episode. This output will be long! =back =item B<-phase=CSV_LIST> This option allows the phases of the copying process to be selected individually. The argument B is a list of phase names, which have to be typed exactly. The order is not important since the script will scan its version of the list of phases in order and will check to see if each has been selected. The phase names are: =over 4 =item B Causes the B table to be filled. =item B Causes the B table to be filled. =item B Causes the B table to be filled. =item B Causes the B table to be filled. =item B Causes the B table to be filled. =item B Causes the B and the B tables to be filled. =item B Causes the B table to be filled. =item B Runs various tasks that can only be carried out after the database has been populated. =back =back =head1 DESCRIPTION A full description of the application and its features. May include numerous subsections (i.e. =head2, =head3, etc.) =head1 DIAGNOSTICS A list of every error and warning message that the application can generate (even the ones that will "never happen"), with a full explanation of each problem, one or more likely causes, and any suggested remedies. If the application generates exit status codes (e.g. under Unix) then list the exit status associated with each error. =head1 CONFIGURATION AND ENVIRONMENT A full explanation of any configuration system(s) used by the application, including the names and locations of any configuration files, and the meaning of any environment variables or properties that can be set. These descriptions must also include details of any configuration language used =head1 DEPENDENCIES A list of all the other modules that this module relies upon, including any restrictions on versions, and an indication whether these required modules are part of the standard Perl distribution, part of the module's distribution, or must be installed separately. =head1 INCOMPATIBILITIES A list of any modules that this module cannot be used in conjunction with. This may be due to name conflicts in the interface, or competition for system or program resources, or due to internal limitations of Perl (for example, many modules that use source code filters are mutually incompatible). =head1 BUGS AND LIMITATIONS A list of known problems with the module, together with some indication whether they are likely to be fixed in an upcoming release. Also a list of restrictions on the features the module does provide: data types that cannot be handled, performance issues and the circumstances in which they may arise, practical limitations on the size of data sets, special cases that are not (yet) handled, etc. The initial template usually just has: There are no known bugs in this module. Please report problems to () Patches are welcome. =head1 AUTHOR () =head1 LICENCE AND COPYRIGHT Copyright (c) (). All rights reserved. Followed by whatever licence you wish to release it under. For Perl code that is often just: This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perldoc perlartistic. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. =cut #}}} # [zo to open fold, zc to close] # vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker